diff --git a/.github/workflows/unit-test-report.yml b/.github/workflows/unit-test-report.yml new file mode 100644 index 0000000..c8a4b72 --- /dev/null +++ b/.github/workflows/unit-test-report.yml @@ -0,0 +1,36 @@ +name: "Dot Net Test Reporter" + +on: + pull_request_target: + types: [ opened, synchronize ] + +permissions: + pull-requests: write + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: | + 8.0.x + - name: Restore dependencies + run: dotnet restore -p:TargetFramework=net8.0 AutomatedTesting + - name: Build-8.0 + run: dotnet build --framework net8.0 --no-restore AutomatedTesting + - name: Test-8.0 + run: dotnet test --framework net8.0 --no-build --verbosity normal AutomatedTesting + - name: report results + uses: bibipkins/dotnet-test-reporter@v1.4.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-title: 'Unit Test Results' + results-path: ./**/*.trx + coverage-path: ./**/coverage.xml + coverage-threshold: 90 \ No newline at end of file diff --git a/Abstractions/Abstractions.csproj b/Abstractions/Abstractions.csproj index 184e465..f6cb5da 100644 --- a/Abstractions/Abstractions.csproj +++ b/Abstractions/Abstractions.csproj @@ -1,26 +1,14 @@  + + net8.0 enable enable MQContract.$(MSBuildProjectName) MQContract - true - $(MSBuildProjectDirectory)\Readme.md - True - roger-castaldo Abstractions for MQContract - $(AssemblyName) - https://github.com/roger-castaldo/MQContract - Readme.md - https://github.com/roger-castaldo/MQContract - Message Queue MQ Contract - MIT - True - True - True - snupkg diff --git a/Abstractions/Attributes/MessageResponseTimeoutAttribute.cs b/Abstractions/Attributes/MessageResponseTimeoutAttribute.cs index a56c79b..015f9ad 100644 --- a/Abstractions/Attributes/MessageResponseTimeoutAttribute.cs +++ b/Abstractions/Attributes/MessageResponseTimeoutAttribute.cs @@ -16,5 +16,10 @@ public class MessageResponseTimeoutAttribute(int value) : Attribute /// The number of milliseconds for the timeout to trigger for this RPC call class /// public int Value => value; + + /// + /// The converted TimeSpan value from the supplied milliseconds value in the constructor + /// + public TimeSpan TimeSpanValue => TimeSpan.FromMilliseconds(value); } } diff --git a/Abstractions/Attributes/QueryResponseChannelAttribute.cs b/Abstractions/Attributes/QueryResponseChannelAttribute.cs new file mode 100644 index 0000000..18db2e7 --- /dev/null +++ b/Abstractions/Attributes/QueryResponseChannelAttribute.cs @@ -0,0 +1,16 @@ +namespace MQContract.Attributes +{ + /// + /// Used to allow the specification of a response channel to be used without supplying it to the contract calls. + /// IMPORTANT: This particular attribute and the response channel argument are only used when the underlying connection does not support QueryResponse messaging. + /// + /// The name of the channel to use for responses + [AttributeUsage(AttributeTargets.Class)] + public class QueryResponseChannelAttribute(string name) : Attribute + { + /// + /// The Name of the response channel + /// + public string Name => name; + } +} diff --git a/Abstractions/Exceptions.cs b/Abstractions/Exceptions.cs deleted file mode 100644 index a3d8013..0000000 --- a/Abstractions/Exceptions.cs +++ /dev/null @@ -1,67 +0,0 @@ -using MQContract.Interfaces.Service; - -namespace MQContract -{ - /// - /// An exception thrown when the options supplied to an underlying system connection are not of an expected type. - /// - public sealed class InvalidChannelOptionsTypeException - : InvalidCastException - { - internal InvalidChannelOptionsTypeException(IEnumerable expectedTypes,Type recievedType) : - base($"Expected Channel Options of Types[{string.Join(',',expectedTypes.Select(t=>t.FullName))}] but recieved {recievedType.FullName}") - { - } - - - internal InvalidChannelOptionsTypeException(Type expectedType, Type recievedType) : - base($"Expected Channel Options of Type {expectedType.FullName} but recieved {recievedType.FullName}") - { - } - - /// - /// Called to check if the options is of a given type - /// - /// The expected type for the ServiceChannelOptions - /// The supplied service channel options - /// Thrown when the options value is not null and not of type T - public static void ThrowIfNotNullAndNotOfType(IServiceChannelOptions? options) - { - if (options!=null && options is not T) - throw new InvalidChannelOptionsTypeException(typeof(T), options.GetType()); - } - - /// - /// Called to check if the options is one of the given types - /// - /// The supplied service channel options - /// The possible types it can be - /// Thrown when the options value is not null and not of any of the expected Types - public static void ThrowIfNotNullAndNotOfType(IServiceChannelOptions? options,IEnumerable expectedTypes) - { - if (options!=null && !expectedTypes.Contains(options.GetType())) - throw new InvalidChannelOptionsTypeException(expectedTypes, options.GetType()); - } - } - - /// - /// An exception thrown when there are options supplied to an underlying system connection that does not support options for that particular instance - /// - public sealed class NoChannelOptionsAvailableException - : Exception - { - internal NoChannelOptionsAvailableException() - : base("There are no service channel options available for this action") { } - - /// - /// Called to throw if options is not null - /// - /// The service channel options that were supplied - /// Thrown when the options is not null - public static void ThrowIfNotNull(IServiceChannelOptions? options) - { - if (options!=null) - throw new NoChannelOptionsAvailableException(); - } - } -} diff --git a/Abstractions/Interfaces/Conversion/IMessageConverter.cs b/Abstractions/Interfaces/Conversion/IMessageConverter.cs index 89b9def..afd2c61 100644 --- a/Abstractions/Interfaces/Conversion/IMessageConverter.cs +++ b/Abstractions/Interfaces/Conversion/IMessageConverter.cs @@ -2,18 +2,18 @@ { /// /// Used to define a message converter. These are called upon if a - /// message is recieved on a channel of type T but it is waiting for + /// message is received on a channel of type T but it is waiting for /// message of type V /// /// The source message type /// The destination message type - public interface IMessageConverter + public interface IMessageConverter { /// /// Called to convert a message from type T to type V /// /// The message to convert /// The source message converted to the destination type V - V Convert(T source); + ValueTask ConvertAsync(T source); } } diff --git a/Abstractions/Interfaces/Encoding/IMessageEncoder.cs b/Abstractions/Interfaces/Encoding/IMessageEncoder.cs index c4d163c..0c77507 100644 --- a/Abstractions/Interfaces/Encoding/IMessageEncoder.cs +++ b/Abstractions/Interfaces/Encoding/IMessageEncoder.cs @@ -13,7 +13,7 @@ public interface IMessageEncoder /// The type of message being encoded /// The message being encoded /// A byte array of the message in it's encoded form that will be transmitted - byte[] Encode(T message); + ValueTask EncodeAsync(T message); /// /// Called to decode a message from a byte array @@ -21,6 +21,6 @@ public interface IMessageEncoder /// The type of message being decoded /// A stream representing the byte array data that was transmitted as the message body in KubeMQ /// Null when fails or the value of T that was encoded inside the stream - T? Decode(Stream stream); + ValueTask DecodeAsync(Stream stream); } } diff --git a/Abstractions/Interfaces/Encoding/IMessageTypeEncoder.cs b/Abstractions/Interfaces/Encoding/IMessageTypeEncoder.cs index ddd7338..726b3a0 100644 --- a/Abstractions/Interfaces/Encoding/IMessageTypeEncoder.cs +++ b/Abstractions/Interfaces/Encoding/IMessageTypeEncoder.cs @@ -12,12 +12,12 @@ public interface IMessageTypeEncoder /// /// The message value to encode /// The message encoded as a byte array - byte[] Encode(T message); + ValueTask EncodeAsync(T message); /// /// Called to decode the message from a byte stream into the specified type /// /// The byte stream containing the encoded message /// null if the Decode fails, otherwise an instance of the message decoded from the stream - T? Decode(Stream stream); + ValueTask DecodeAsync(Stream stream); } } diff --git a/Abstractions/Interfaces/Encrypting/IMessageEncryptor.cs b/Abstractions/Interfaces/Encrypting/IMessageEncryptor.cs index 6b1d20d..bbfdc75 100644 --- a/Abstractions/Interfaces/Encrypting/IMessageEncryptor.cs +++ b/Abstractions/Interfaces/Encrypting/IMessageEncryptor.cs @@ -10,12 +10,12 @@ namespace MQContract.Interfaces.Encrypting public interface IMessageEncryptor { /// - /// Called to decrypt the message body stream recieved as a message + /// Called to decrypt the message body stream received as a message /// /// The stream representing the message body binary data /// The message headers that were provided by the message /// A decrypted stream of the message body - Stream Decrypt(Stream stream, MessageHeader headers); + ValueTask DecryptAsync(Stream stream, MessageHeader headers); /// /// Called to encrypt the message body prior to transmitting a message @@ -23,6 +23,6 @@ public interface IMessageEncryptor /// The original unencrypted body data /// The headers that are desired to attache to the message if needed /// An encrypted byte array of the message body - byte[] Encrypt(byte[] data, out Dictionary headers); + ValueTask EncryptAsync(byte[] data, out Dictionary headers); } } diff --git a/Abstractions/Interfaces/IContractConnection.cs b/Abstractions/Interfaces/IContractConnection.cs index d485437..d64a8f3 100644 --- a/Abstractions/Interfaces/IContractConnection.cs +++ b/Abstractions/Interfaces/IContractConnection.cs @@ -1,18 +1,109 @@ -using MQContract.Interfaces.Service; +using MQContract.Interfaces.Middleware; using MQContract.Messages; +using System.Diagnostics.Metrics; namespace MQContract.Interfaces { /// /// This interface represents the Core class for the MQContract system, IE the ContractConnection /// - public interface IContractConnection + public interface IContractConnection : IDisposable,IAsyncDisposable { + /// + /// Register a middleware of a given type T to be used by the contract connection + /// + /// The type of middle ware to register, it must implement IBeforeDecodeMiddleware or IBeforeEncodeMiddleware or IAfterDecodeMiddleware or IAfterEncodeMiddleware + /// The Contract Connection instance to allow chaining calls + IContractConnection RegisterMiddleware() + where T : IMiddleware; + /// + /// Register a middleware of a given type T to be used by the contract connection + /// + /// Callback to create the instance + /// The type of middle ware to register, it must implement IBeforeDecodeMiddleware or IBeforeEncodeMiddleware or IAfterDecodeMiddleware or IAfterEncodeMiddleware + /// The Contract Connection instance to allow chaining calls + IContractConnection RegisterMiddleware(Func constructInstance) + where T : IMiddleware; + /// + /// Register a middleware of a given type T to be used by the contract connection + /// + /// The type of middle ware to register, it must implement IBeforeEncodeSpecificTypeMiddleware<M> or IAfterDecodeSpecificTypeMiddleware<M> + /// The message type that this middleware is specifically called for + /// The Contract Connection instance to allow chaining calls + IContractConnection RegisterMiddleware() + where T : ISpecificTypeMiddleware + where M : class; + /// + /// Register a middleware of a given type T to be used by the contract connection + /// + /// Callback to create the instance + /// The type of middle ware to register, it must implement IBeforeEncodeSpecificTypeMiddleware<M> or IAfterDecodeSpecificTypeMiddleware<M> + /// The message type that this middleware is specifically called for + /// The Contract Connection instance to allow chaining calls + IContractConnection RegisterMiddleware(Func constructInstance) + where T : ISpecificTypeMiddleware + where M : class; + /// + /// Called to activate the metrics tracking middleware for this connection instance + /// + /// The Meter item to create all system metrics against + /// Indicates if the internal metrics collector should be used + /// The Contract Connection instance to allow chaining calls + /// + /// For the Meter metrics, all durations are in ms and the following values and patterns will apply: + /// mqcontract.messages.sent.count = count of messages sent (Counter<long>) + /// mqcontract.messages.sent.bytes = count of bytes sent (message data) (Counter<long>) + /// mqcontract.messages.received.count = count of messages received (Counter<long>) + /// mqcontract.messages.received.bytes = count of bytes received (message data) (Counter<long>) + /// mqcontract.messages.encodingduration = milliseconds to encode messages (Histogram<double>) + /// mqcontract.messages.decodingduration = milliseconds to decode messages (Histogram<double>) + /// mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.sent.count = count of messages sent of a given type (Counter<long>) + /// mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.sent.bytes = count of bytes sent (message data) of a given type (Counter<long>) + /// mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.received.count = count of messages received of a given type (Counter<long>) + /// mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.received.bytes = count of bytes received (message data) of a given type (Counter<long>) + /// mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.encodingduration = milliseconds to encode messages of a given type (Histogram<double>) + /// mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.decodingduration = milliseconds to decode messages of a given type (Histogram<double>) + /// mqcontract.channels.{Channel}.sent.count = count of messages sent for a given channel (Counter<long>) + /// mqcontract.channels.{Channel}.sent.bytes = count of bytes sent (message data) for a given channel (Counter<long>) + /// mqcontract.channels.{Channel}.received.count = count of messages received for a given channel (Counter<long>) + /// mqcontract.channels.{Channel}.received.bytes = count of bytes received (message data) for a given channel (Counter<long>) + /// mqcontract.channels.{Channel}.encodingduration = milliseconds to encode messages for a given channel (Histogram<double>) + /// mqcontract.channels.{Channel}.decodingduration = milliseconds to decode messages for a given channel (Histogram<double>) + /// + IContractConnection AddMetrics(Meter? meter, bool useInternal); + /// + /// Called to get a snapshot of the current global metrics. Will return null if internal metrics are not enabled. + /// + /// true when the sent metrics are desired, false when received are desired + /// A record of the current metric snapshot or null if not available + IContractMetric? GetSnapshot(bool sent); + /// + /// Called to get a snapshot of the metrics for a given message type. Will return null if internal metrics are not enabled. + /// + /// The type of message to look for + /// true when the sent metrics are desired, false when received are desired + /// A record of the current metric snapshot or null if not available + IContractMetric? GetSnapshot(Type messageType,bool sent); + /// + /// Called to get a snapshot of the metrics for a given message type. Will return null if internal metrics are not enabled. + /// + /// The type of message to look for + /// true when the sent metrics are desired, false when received are desired + /// A record of the current metric snapshot or null if not available + IContractMetric? GetSnapshot(bool sent) + where T : class; + /// + /// Called to get a snapshot of the metrics for a given message channel. Will return null if internal metrics are not enabled. + /// + /// The channel to look for + /// true when the sent metrics are desired, false when received are desired + /// A record of the current metric snapshot or null if not available + IContractMetric? GetSnapshot(string channel,bool sent); /// /// Called to Ping the underlying system to obtain both information and ensure it is up. Not all Services support this method. /// /// - Task PingAsync(); + ValueTask PingAsync(); /// /// Called to send a message into the underlying service Pub/Sub style /// @@ -20,25 +111,38 @@ public interface IContractConnection /// The message to send /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. /// The headers to pass along with the message - /// Any required Service Channel Options that will be passed down to the service Connection /// A cancellation token + /// /// A result indicating the tranmission results - Task PublishAsync(T message, string? channel = null, MessageHeader? messageHeader = null, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) + ValueTask PublishAsync(T message, string? channel = null, MessageHeader? messageHeader = null, CancellationToken cancellationToken = new CancellationToken()) where T : class; /// - /// Called to create a subscription into the underlying service Pub/Sub style + /// Called to create a subscription into the underlying service Pub/Sub style and have the messages processed asynchronously /// /// The type of message to listen for - /// The callback invoked when a new message is recieved - /// The callback to invoke when an error occurs + /// The callback invoked when a new message is received + /// The callback to invoke when an error occurs /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. /// The subscription group if desired (typically used when multiple instances of the same system are running) /// If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class - /// Inddicates if the callbacks for a recieved message should be called synchronously or asynchronously - /// Any required Service Channel Options that will be passed down to the service Connection /// A cancellation token + /// /// A subscription instance that can be ended when desired - Task SubscribeAsync(Func,Task> messageRecieved, Action errorRecieved, string? channel = null, string? group = null, bool ignoreMessageHeader = false,bool synchronous=false, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) + ValueTask SubscribeAsync(Func, ValueTask> messageReceived, Action errorReceived, string? channel = null, string? group = null, bool ignoreMessageHeader = false, CancellationToken cancellationToken = new CancellationToken()) + where T : class; + /// + /// Called to create a subscription into the underlying service Pub/Sub style and have the messages processed syncrhonously + /// + /// The type of message to listen for + /// The callback invoked when a new message is received + /// The callback to invoke when an error occurs + /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. + /// The subscription group if desired (typically used when multiple instances of the same system are running) + /// If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class + /// A cancellation token + /// + /// A subscription instance that can be ended when desired + ValueTask SubscribeAsync(Action> messageReceived, Action errorReceived, string? channel = null, string? group = null, bool ignoreMessageHeader = false, CancellationToken cancellationToken = new CancellationToken()) where T : class; /// /// Called to send a message into the underlying service in the Query/Response style @@ -46,13 +150,15 @@ public interface IContractConnection /// The type of message to send for the query /// The type of message to expect back for the response /// The message to send - /// The allowed timeout prior to a response being recieved + /// The allowed timeout prior to a response being received /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. + /// Specifies the message channel to use for the response. The preferred method is using the QueryResponseChannelAttribute on the class. This is + /// only used when the underlying connection does not support a QueryResponse style messaging. /// The headers to pass along with the message - /// Any required Service Channel Options that will be passed down to the service Connection /// A cancellation token + /// /// A result indicating the success or failure as well as the returned message - Task> QueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, MessageHeader? messageHeader = null, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) + ValueTask> QueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, string? responseChannel = null, MessageHeader? messageHeader = null, CancellationToken cancellationToken = new CancellationToken()) where Q : class where R : class; /// @@ -61,30 +167,52 @@ public interface IContractConnection /// /// The type of message to send for the query /// The message to send - /// The allowed timeout prior to a response being recieved + /// The allowed timeout prior to a response being received /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. + /// /// Specifies the message channel to use for the response. The preferred method is using the QueryResponseChannelAttribute on the class. This is + /// only used when the underlying connection does not support a QueryResponse style messaging. /// The headers to pass along with the message - /// Any required Service Channel Options that will be passed down to the service Connection /// A cancellation token + /// /// A result indicating the success or failure as well as the returned message - Task> QueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, MessageHeader? messageHeader = null, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) + ValueTask> QueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, string? responseChannel = null, MessageHeader? messageHeader = null, CancellationToken cancellationToken = new CancellationToken()) where Q : class; /// - /// Called to create a subscription into the underlying service Query/Reponse style + /// Called to create a subscription into the underlying service Query/Reponse style and have the messages processed asynchronously + /// + /// The type of message to listen for + /// The type of message to respond with + /// The callback invoked when a new message is received expecting a response of the type response + /// The callback invoked when an error occurs. + /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. + /// The subscription group if desired (typically used when multiple instances of the same system are running) + /// If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class + /// A cancellation token + /// + /// A subscription instance that can be ended when desired + ValueTask SubscribeQueryAsyncResponseAsync(Func, ValueTask>> messageReceived, Action errorReceived, string? channel = null, string? group = null, bool ignoreMessageHeader = false, CancellationToken cancellationToken = new CancellationToken()) + where Q : class + where R : class; + /// + /// Called to create a subscription into the underlying service Query/Reponse style and have the messages processed synchronously /// /// The type of message to listen for /// The type of message to respond with - /// The callback invoked when a new message is recieved expecting a response of the type response - /// The callback invoked when an error occurs. + /// The callback invoked when a new message is received expecting a response of the type response + /// The callback invoked when an error occurs. /// Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. /// The subscription group if desired (typically used when multiple instances of the same system are running) /// If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class - /// Inddicates if the callbacks for a recieved message should be called synchronously or asynchronously - /// Any required Service Channel Options that will be passed down to the service Connection /// A cancellation token + /// /// A subscription instance that can be ended when desired - Task SubscribeQueryResponseAsync(Func,Task>> messageRecieved, Action errorRecieved, string? channel = null, string? group = null, bool ignoreMessageHeader = false, bool synchronous = false, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) + ValueTask SubscribeQueryResponseAsync(Func, QueryResponseMessage> messageReceived, Action errorReceived, string? channel = null, string? group = null, bool ignoreMessageHeader = false, CancellationToken cancellationToken = new CancellationToken()) where Q : class where R : class; + /// + /// Called to close off the contract connection and close it's underlying service connection + /// + /// A task for the closure of the connection + ValueTask CloseAsync(); } } diff --git a/Abstractions/Interfaces/IContractMetric.cs b/Abstractions/Interfaces/IContractMetric.cs new file mode 100644 index 0000000..f39ad25 --- /dev/null +++ b/Abstractions/Interfaces/IContractMetric.cs @@ -0,0 +1,48 @@ +namespace MQContract.Interfaces +{ + /// + /// Houses a set of metrics that were requested from the internal metric tracker. + /// All message conversion durations are calculated from the perspective: + /// - When a class is being sent from the point of starting the middleware to the point where the class has been encoded into a service message and the middleware has completed + /// - When a service message is being recieved from the point of starting the middleware to the point where the class has been built from the service message and the middleware has completed + /// + public interface IContractMetric + { + /// + /// Total number of messages + /// + ulong Messages { get; } + /// + /// Total amount of bytes from the messages + /// + ulong MessageBytes { get; } + /// + /// Average number of bytes from the messages + /// + ulong MessageBytesAverage { get; } + /// + /// Minimum number of bytes from the messages + /// + ulong MessageBytesMin { get; } + /// + /// Maximum number of bytes from the messages + /// + ulong MessageBytesMax { get; } + /// + /// Total time spent converting the messages + /// + TimeSpan MessageConversionDuration { get; } + /// + /// Average time to encode/decode the messages + /// + TimeSpan MessageConversionAverage { get; } + /// + /// Minimum time to encode/decode a message + /// + TimeSpan MessageConversionMin { get; } + /// + /// Maximum time to encode/decode a message + /// + TimeSpan MessageConversionMax { get; } + } +} diff --git a/Abstractions/Interfaces/IRecievedMessage.cs b/Abstractions/Interfaces/IRecievedMessage.cs index 5cec6f3..ef94e4e 100644 --- a/Abstractions/Interfaces/IRecievedMessage.cs +++ b/Abstractions/Interfaces/IRecievedMessage.cs @@ -3,14 +3,14 @@ namespace MQContract.Interfaces { /// - /// An interface for describing a Message recieved on a Subscription to be passed into the appropriate callback + /// An interface for describing a Message received on a Subscription to be passed into the appropriate callback /// /// The class type of the underlying message - public interface IRecievedMessage + public interface IReceivedMessage where T : class { /// - /// The unique ID of the recieved message that was specified on the transmission side + /// The unique ID of the received message that was specified on the transmission side /// string ID { get; } /// @@ -22,11 +22,11 @@ public interface IRecievedMessage /// MessageHeader Headers { get; } /// - /// The timestamp of when the message was recieved by the underlying service connection + /// The timestamp of when the message was received by the underlying service connection /// - DateTime RecievedTimestamp { get; } + DateTime ReceivedTimestamp { get; } /// - /// The timestamp of when the recieved message was converted into the actual class prior to calling the callback + /// The timestamp of when the received message was converted into the actual class prior to calling the callback /// DateTime ProcessedTimestamp { get; } } diff --git a/Abstractions/Interfaces/ISubscription.cs b/Abstractions/Interfaces/ISubscription.cs index 3a92f16..cc6bb7b 100644 --- a/Abstractions/Interfaces/ISubscription.cs +++ b/Abstractions/Interfaces/ISubscription.cs @@ -3,12 +3,12 @@ /// /// This interface represents a Contract Connection Subscription and is used to house and end the subscription /// - public interface ISubscription : IDisposable + public interface ISubscription : IDisposable, IAsyncDisposable { /// /// Called to end (close off) the subscription /// /// A task that is ending the subscription and closing off the resources for it - Task EndAsync(); + ValueTask EndAsync(); } } diff --git a/Abstractions/Interfaces/Middleware/IAfterDecodeMiddleware.cs b/Abstractions/Interfaces/Middleware/IAfterDecodeMiddleware.cs new file mode 100644 index 0000000..6687a9f --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IAfterDecodeMiddleware.cs @@ -0,0 +1,23 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Middleware +{ + /// + /// This interface represents a Middleware to execute after a Message has been decoded from a ServiceMessage to the expected Class + /// + public interface IAfterDecodeMiddleware : IMiddleware + { + /// + /// This is the method invoked as part of the Middleware processing during message decoding + /// + /// This will be the type of the Message that was decoded + /// A shared context that exists from the start of this decode process instance + /// The class message + /// The id of the message + /// The headers from the message + /// The timestamp of when the message was recieved + /// The timestamp of when the message was decoded into a Class + /// The message and header to allow for changes if desired + ValueTask<(T message,MessageHeader messageHeader)> AfterMessageDecodeAsync(IContext context, T message, string ID,MessageHeader messageHeader,DateTime receivedTimestamp,DateTime processedTimeStamp); + } +} diff --git a/Abstractions/Interfaces/Middleware/IAfterDecodeSpecificTypeMiddleware.cs b/Abstractions/Interfaces/Middleware/IAfterDecodeSpecificTypeMiddleware.cs new file mode 100644 index 0000000..d976e85 --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IAfterDecodeSpecificTypeMiddleware.cs @@ -0,0 +1,23 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Middleware +{ + /// + /// This interface represents a Middleware to execute after a Message of the given type T has been decoded from a ServiceMessage to the expected Class + /// + public interface IAfterDecodeSpecificTypeMiddleware : ISpecificTypeMiddleware + where T : class + { + /// + /// This is the method invoked as part of the Middleware processing during message decoding + /// + /// A shared context that exists from the start of this decode process instance + /// The class message + /// The id of the message + /// The headers from the message + /// The timestamp of when the message was recieved + /// The timestamp of when the message was decoded into a Class + /// The message and header to allow for changes if desired + ValueTask<(T message,MessageHeader messageHeader)> AfterMessageDecodeAsync(IContext context, T message, string ID,MessageHeader messageHeader,DateTime receivedTimestamp,DateTime processedTimeStamp); + } +} diff --git a/Abstractions/Interfaces/Middleware/IAfterEncodeMiddleware.cs b/Abstractions/Interfaces/Middleware/IAfterEncodeMiddleware.cs new file mode 100644 index 0000000..c7b6c45 --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IAfterEncodeMiddleware.cs @@ -0,0 +1,19 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Middleware +{ + /// + /// This interface represents a Middleware to execute after a Message has been encoded to a ServiceMessage from the supplied Class + /// + public interface IAfterEncodeMiddleware : IMiddleware + { + /// + /// This is the method invoked as part of the Middleware processing during message encoding + /// + /// The class of the message type that was encoded + /// A shared context that exists from the start of this encode process instance + /// The resulting encoded message + /// The message to allow for changes if desired + ValueTask AfterMessageEncodeAsync(Type messageType, IContext context, ServiceMessage message); + } +} diff --git a/Abstractions/Interfaces/Middleware/IBeforeDecodeMiddleware.cs b/Abstractions/Interfaces/Middleware/IBeforeDecodeMiddleware.cs new file mode 100644 index 0000000..854b35f --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IBeforeDecodeMiddleware.cs @@ -0,0 +1,22 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Middleware +{ + /// + /// This interface represents a Middleware to execute before decoding a ServiceMessage + /// + public interface IBeforeDecodeMiddleware : IMiddleware + { + /// + /// This is the method invoked as part of the Middleware processing prior to the message decoding + /// + /// A shared context that exists from the start of this decode process instance + /// The id of the message + /// The headers from the message + /// The message type id + /// The channel the message was recieved on + /// The data of the message + /// The message header and data to allow for changes if desired + ValueTask<(MessageHeader messageHeader,ReadOnlyMemory data)> BeforeMessageDecodeAsync(IContext context, string id, MessageHeader messageHeader, string messageTypeID,string messageChannel, ReadOnlyMemory data); + } +} diff --git a/Abstractions/Interfaces/Middleware/IBeforeEncodeMiddleware.cs b/Abstractions/Interfaces/Middleware/IBeforeEncodeMiddleware.cs new file mode 100644 index 0000000..0413d92 --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IBeforeEncodeMiddleware.cs @@ -0,0 +1,21 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Middleware +{ + /// + /// This interface represents a Middleware to execute Before a message is encoded + /// + public interface IBeforeEncodeMiddleware : IMiddleware + { + /// + /// This is the method invoked as part of the Middle Ware processing during message encoding + /// + /// The type of message being processed + /// A shared context that exists from the start of this encoding instance + /// The message being encoded + /// The channel this message was requested to transmit to + /// The message headers being supplied + /// The message, channel and header to allow for changes if desired + ValueTask<(T message,string? channel,MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context,T message, string? channel,MessageHeader messageHeader); + } +} diff --git a/Abstractions/Interfaces/Middleware/IBeforeEncodeSpecificTypeMiddleware.cs b/Abstractions/Interfaces/Middleware/IBeforeEncodeSpecificTypeMiddleware.cs new file mode 100644 index 0000000..50f914a --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IBeforeEncodeSpecificTypeMiddleware.cs @@ -0,0 +1,21 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Middleware +{ + /// + /// This interface represents a Middleware to execute Before a specific message type is encoded + /// + public interface IBeforeEncodeSpecificTypeMiddleware : ISpecificTypeMiddleware + where T : class + { + /// + /// This is the method invoked as part of the Middle Ware processing during message encoding + /// + /// A shared context that exists from the start of this encoding instance + /// The message being encoded + /// The channel this message was requested to transmit to + /// The message headers being supplied + /// The message, channel and header to allow for changes if desired + ValueTask<(T message,string? channel,MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context,T message, string? channel,MessageHeader messageHeader); + } +} diff --git a/Abstractions/Interfaces/Middleware/IContext.cs b/Abstractions/Interfaces/Middleware/IContext.cs new file mode 100644 index 0000000..db7f792 --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IContext.cs @@ -0,0 +1,15 @@ +namespace MQContract.Interfaces.Middleware +{ + /// + /// This is used to represent a Context for the middleware calls to use that exists from the start to the end of the message conversion process + /// + public interface IContext + { + /// + /// Used to store and retreive values from the context during the conversion process. + /// + /// The unique key to use + /// The value if it exists in the context + object? this[string key] { get; set; } + } +} diff --git a/Abstractions/Interfaces/Middleware/IMiddleware.cs b/Abstractions/Interfaces/Middleware/IMiddleware.cs new file mode 100644 index 0000000..22b0fee --- /dev/null +++ b/Abstractions/Interfaces/Middleware/IMiddleware.cs @@ -0,0 +1,9 @@ +namespace MQContract.Interfaces.Middleware +{ + /// + /// Base Middleware just used to limit Generic Types for Register Middleware + /// + public interface IMiddleware + { + } +} diff --git a/Abstractions/Interfaces/Middleware/ISpecificTypeMiddleware.cs b/Abstractions/Interfaces/Middleware/ISpecificTypeMiddleware.cs new file mode 100644 index 0000000..af90822 --- /dev/null +++ b/Abstractions/Interfaces/Middleware/ISpecificTypeMiddleware.cs @@ -0,0 +1,10 @@ +namespace MQContract.Interfaces.Middleware +{ + /// + /// Base Specific Type Middleware just used to limit Generic Types for Register Middleware + /// + public interface ISpecificTypeMiddleware + where T : class + { + } +} diff --git a/Abstractions/Interfaces/Service/IInboxQueryableMessageServiceConnection.cs b/Abstractions/Interfaces/Service/IInboxQueryableMessageServiceConnection.cs new file mode 100644 index 0000000..20f9015 --- /dev/null +++ b/Abstractions/Interfaces/Service/IInboxQueryableMessageServiceConnection.cs @@ -0,0 +1,27 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Service +{ + /// + /// Used to implement an Inbox style query response underlying service, this is if the service does not support QueryResponse messaging but does support a sort of query inbox response + /// style pub sub where you can specify the destination down to a specific instance. + /// + public interface IInboxQueryableMessageServiceConnection : IQueryableMessageServiceConnection + { + /// + /// Establish the inbox subscription with the underlying service connection + /// + /// Callback called when a message is recieved in the RPC inbox + /// A cancellation token + /// A service subscription object specifically tied to the RPC inbox for this particular connection instance + ValueTask EstablishInboxSubscriptionAsync(Action messageReceived,CancellationToken cancellationToken = new CancellationToken()); + /// + /// Called to publish a Query Request when using the inbox style + /// + /// The service message to submit + /// The unique ID of the message to use for handling when the response is proper and is expected in the inbox subscription + /// A cancellation token + /// The transmission result of submitting the message + ValueTask QueryAsync(ServiceMessage message,Guid correlationID, CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/Abstractions/Interfaces/Service/IMessageServiceConnection.cs b/Abstractions/Interfaces/Service/IMessageServiceConnection.cs index 1d3e565..b1d21ad 100644 --- a/Abstractions/Interfaces/Service/IMessageServiceConnection.cs +++ b/Abstractions/Interfaces/Service/IMessageServiceConnection.cs @@ -6,59 +6,34 @@ namespace MQContract.Interfaces.Service /// Defines an underlying service connection. This interface is used to allow for the creation of multiple underlying connection types to support the ability to use common code while /// being able to run against 1 or more Message services. /// - public interface IMessageServiceConnection: IDisposable + public interface IMessageServiceConnection { /// /// Maximum supported message body size in bytes /// - int? MaxMessageBodySize { get; } - /// - /// The default timeout to use for RPC calls when it's not specified - /// - TimeSpan DefaultTimout { get; } - /// - /// Implemented Ping call if avaialble for the underlying service - /// - /// A Ping Result - Task PingAsync(); + uint? MaxMessageBodySize { get; } /// /// Implements a publish call to publish the given message /// /// The message to publish - /// The Service Channel Options instance that was supplied at the Contract Connection level /// A cancellation token + /// /// A transmission result instance indicating the result - Task PublishAsync(ServiceMessage message, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()); + ValueTask PublishAsync(ServiceMessage message, CancellationToken cancellationToken = new CancellationToken()); /// /// Implements a call to create a subscription to a given channel as a member of a given group /// - /// The callback to invoke when a message is recieved - /// The callback to invoke when an exception occurs + /// The callback to invoke when a message is received + /// The callback to invoke when an exception occurs /// The name of the channel to subscribe to - /// The subscription groupt to subscribe as - /// The Service Channel Options instance that was supplied at the Contract Connection level + /// The consumer group to register as /// A cancellation token /// A service subscription object - Task SubscribeAsync(Action messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()); - /// - /// Implements a call to submit a response query request into the underlying service - /// - /// The message to query with - /// The timeout for recieving a response - /// The Service Channel Options instance that was supplied at the Contract Connection level - /// A cancellation token - /// A Query Result instance based on what happened - Task QueryAsync(ServiceMessage message, TimeSpan timeout, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()); + ValueTask SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group = null, CancellationToken cancellationToken = new CancellationToken()); /// - /// Implements a call to create a subscription to a given channel as a member of a given group for responding to queries + /// Implements a call to close off the connection when the ContractConnection is closed /// - /// The callback to be invoked when a message is recieved, returning the response message - /// The callback to invoke when an exception occurs - /// The name of the channel to subscribe to - /// The subscription groupt to subscribe as - /// The Service Channel Options instance that was supplied at the Contract Connection level - /// A cancellation token - /// A service subscription object - Task SubscribeQueryAsync(Func> messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()); + /// A task that the close is running in + ValueTask CloseAsync(); } } diff --git a/Abstractions/Interfaces/Service/IPingableMessageServiceConnection.cs b/Abstractions/Interfaces/Service/IPingableMessageServiceConnection.cs new file mode 100644 index 0000000..4dee87c --- /dev/null +++ b/Abstractions/Interfaces/Service/IPingableMessageServiceConnection.cs @@ -0,0 +1,16 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Service +{ + /// + /// Extends the base MessageServiceConnection Interface to support service pinging + /// + public interface IPingableMessageServiceConnection : IMessageServiceConnection + { + /// + /// Implemented Ping call if avaialble for the underlying service + /// + /// A Ping Result + ValueTask PingAsync(); + } +} diff --git a/Abstractions/Interfaces/Service/IQueryResponseMessageServiceConnection.cs b/Abstractions/Interfaces/Service/IQueryResponseMessageServiceConnection.cs new file mode 100644 index 0000000..5d3463d --- /dev/null +++ b/Abstractions/Interfaces/Service/IQueryResponseMessageServiceConnection.cs @@ -0,0 +1,19 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Service +{ + /// + /// Extends the base MessageServiceConnection Interface to Response Query messaging methodology if the underlying service supports it + /// + public interface IQueryResponseMessageServiceConnection : IQueryableMessageServiceConnection + { + /// + /// Implements a call to submit a response query request into the underlying service + /// + /// The message to query with + /// The timeout for recieving a response + /// A cancellation token + /// A Query Result instance based on what happened + ValueTask QueryAsync(ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/Abstractions/Interfaces/Service/IQueryableMessageServiceConnection.cs b/Abstractions/Interfaces/Service/IQueryableMessageServiceConnection.cs new file mode 100644 index 0000000..8569ec5 --- /dev/null +++ b/Abstractions/Interfaces/Service/IQueryableMessageServiceConnection.cs @@ -0,0 +1,25 @@ +using MQContract.Messages; + +namespace MQContract.Interfaces.Service +{ + /// + /// Used to identify a message service that supports response query style messaging, either through inbox or directly + /// + public interface IQueryableMessageServiceConnection : IMessageServiceConnection + { + /// + /// The default timeout to use for RPC calls when it's not specified + /// + TimeSpan DefaultTimeout { get; } + /// + /// Implements a call to create a subscription to a given channel as a member of a given group for responding to queries + /// + /// The callback to be invoked when a message is received, returning the response message + /// The callback to invoke when an exception occurs + /// The name of the channel to subscribe to + /// The group to bind a consumer to + /// A cancellation token + /// A service subscription object + ValueTask SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group = null, CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/Abstractions/Interfaces/Service/IServiceChannelOptions.cs b/Abstractions/Interfaces/Service/IServiceChannelOptions.cs deleted file mode 100644 index fb93d4f..0000000 --- a/Abstractions/Interfaces/Service/IServiceChannelOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MQContract.Interfaces.Service -{ - /// - /// Used to pass service channel options to the underlying service connection. There are no implemented values this is simply mean to be a class marker. - /// - public interface IServiceChannelOptions - { - } -} diff --git a/Abstractions/Interfaces/Service/IServiceSubscription.cs b/Abstractions/Interfaces/Service/IServiceSubscription.cs index 160ac7d..1d238c4 100644 --- a/Abstractions/Interfaces/Service/IServiceSubscription.cs +++ b/Abstractions/Interfaces/Service/IServiceSubscription.cs @@ -3,12 +3,12 @@ /// /// Represents an underlying service level subscription /// - public interface IServiceSubscription : IDisposable + public interface IServiceSubscription { /// /// Called to end the subscription /// /// A task to allow for asynchronous ending of the subscription - Task EndAsync(); + ValueTask EndAsync(); } } diff --git a/Abstractions/Messages/MessageHeader.cs b/Abstractions/Messages/MessageHeader.cs index 4f0ecf5..feedade 100644 --- a/Abstractions/Messages/MessageHeader.cs +++ b/Abstractions/Messages/MessageHeader.cs @@ -20,8 +20,12 @@ public MessageHeader(Dictionary? headers) /// The additional properties to add public MessageHeader(MessageHeader? originalHeader, Dictionary? appendedHeader) : this( - (appendedHeader?.AsEnumerable().Select(pair => new KeyValuePair(pair.Key, pair.Value??string.Empty))?? []) - .Concat(originalHeader?.Keys.Select(k => new KeyValuePair(k, originalHeader?[k]!))?? []) + (appendedHeader?.AsEnumerable() + .Where(pair=>pair.Value!=null) + .Select(pair => new KeyValuePair(pair.Key, pair.Value!))?? []) + .Concat(originalHeader?.Keys + .Where(k => !(appendedHeader?? []).Any(pair=>Equals(k,pair.Key))) + .Select(k => new KeyValuePair(k, originalHeader?[k]!))?? []) .DistinctBy(k => k.Key) ) { } diff --git a/Abstractions/Messages/RecievedInboxServiceMessage.cs b/Abstractions/Messages/RecievedInboxServiceMessage.cs new file mode 100644 index 0000000..fd5f64f --- /dev/null +++ b/Abstractions/Messages/RecievedInboxServiceMessage.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MQContract.Messages +{ + /// + /// A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection + /// + /// The unique ID of the message + /// The message type id which is used for decoding to a class + /// The channel the message was received on + /// The message headers that came through + /// The query message correlation id supplied by the query call to tie to the response + /// The binary content of the message that should be the encoded class + /// The acknowledgement callback to be called when the message is received if the underlying service requires it + [ExcludeFromCodeCoverage(Justification ="This is a record class and has nothing to test")] + public record ReceivedInboxServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header,Guid CorrelationID, ReadOnlyMemory Data,Func? Acknowledge=null) + : ReceivedServiceMessage(ID,MessageTypeID,Channel,Header,Data,Acknowledge) + {} +} diff --git a/Abstractions/Messages/RecievedServiceMessage.cs b/Abstractions/Messages/RecievedServiceMessage.cs index 8ffcb56..d8b573a 100644 --- a/Abstractions/Messages/RecievedServiceMessage.cs +++ b/Abstractions/Messages/RecievedServiceMessage.cs @@ -1,19 +1,23 @@ -namespace MQContract.Messages +using System.Diagnostics.CodeAnalysis; + +namespace MQContract.Messages { /// - /// A Recieved Service Message that gets passed back up into the Contract Connection when a message is recieved from the underlying service connection + /// A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection /// /// The unique ID of the message /// The message type id which is used for decoding to a class - /// The channel the message was recieved on + /// The channel the message was received on /// The message headers that came through /// The binary content of the message that should be the encoded class - public record RecievedServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, ReadOnlyMemory Data) + /// The acknowledgement callback to be called when the message is received if the underlying service requires it + [ExcludeFromCodeCoverage(Justification ="This is a record class and has nothing to test")] + public record ReceivedServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, ReadOnlyMemory Data,Func? Acknowledge=null) : ServiceMessage(ID,MessageTypeID,Channel,Header,Data) { /// - /// A timestamp for when the message was recieved + /// A timestamp for when the message was received /// - public DateTime RecievedTimestamp { get; private init; } = DateTime.Now; + public DateTime ReceivedTimestamp { get; private init; } = DateTime.Now; } } diff --git a/Abstractions/Readme.md b/Abstractions/Readme.md index 7919b03..a89e0d5 100644 --- a/Abstractions/Readme.md +++ b/Abstractions/Readme.md @@ -3,51 +3,92 @@ ## Contents +- [IAfterDecodeMiddleware](#T-MQContract-Interfaces-Middleware-IAfterDecodeMiddleware 'MQContract.Interfaces.Middleware.IAfterDecodeMiddleware') + - [AfterMessageDecodeAsync\`\`1(context,message,ID,messageHeader,receivedTimestamp,processedTimeStamp)](#M-MQContract-Interfaces-Middleware-IAfterDecodeMiddleware-AfterMessageDecodeAsync``1-MQContract-Interfaces-Middleware-IContext,``0,System-String,MQContract-Messages-MessageHeader,System-DateTime,System-DateTime- 'MQContract.Interfaces.Middleware.IAfterDecodeMiddleware.AfterMessageDecodeAsync``1(MQContract.Interfaces.Middleware.IContext,``0,System.String,MQContract.Messages.MessageHeader,System.DateTime,System.DateTime)') +- [IAfterDecodeSpecificTypeMiddleware\`1](#T-MQContract-Interfaces-Middleware-IAfterDecodeSpecificTypeMiddleware`1 'MQContract.Interfaces.Middleware.IAfterDecodeSpecificTypeMiddleware`1') + - [AfterMessageDecodeAsync(context,message,ID,messageHeader,receivedTimestamp,processedTimeStamp)](#M-MQContract-Interfaces-Middleware-IAfterDecodeSpecificTypeMiddleware`1-AfterMessageDecodeAsync-MQContract-Interfaces-Middleware-IContext,`0,System-String,MQContract-Messages-MessageHeader,System-DateTime,System-DateTime- 'MQContract.Interfaces.Middleware.IAfterDecodeSpecificTypeMiddleware`1.AfterMessageDecodeAsync(MQContract.Interfaces.Middleware.IContext,`0,System.String,MQContract.Messages.MessageHeader,System.DateTime,System.DateTime)') +- [IAfterEncodeMiddleware](#T-MQContract-Interfaces-Middleware-IAfterEncodeMiddleware 'MQContract.Interfaces.Middleware.IAfterEncodeMiddleware') + - [AfterMessageEncodeAsync(messageType,context,message)](#M-MQContract-Interfaces-Middleware-IAfterEncodeMiddleware-AfterMessageEncodeAsync-System-Type,MQContract-Interfaces-Middleware-IContext,MQContract-Messages-ServiceMessage- 'MQContract.Interfaces.Middleware.IAfterEncodeMiddleware.AfterMessageEncodeAsync(System.Type,MQContract.Interfaces.Middleware.IContext,MQContract.Messages.ServiceMessage)') +- [IBeforeDecodeMiddleware](#T-MQContract-Interfaces-Middleware-IBeforeDecodeMiddleware 'MQContract.Interfaces.Middleware.IBeforeDecodeMiddleware') + - [BeforeMessageDecodeAsync(context,id,messageHeader,messageTypeID,messageChannel,data)](#M-MQContract-Interfaces-Middleware-IBeforeDecodeMiddleware-BeforeMessageDecodeAsync-MQContract-Interfaces-Middleware-IContext,System-String,MQContract-Messages-MessageHeader,System-String,System-String,System-ReadOnlyMemory{System-Byte}- 'MQContract.Interfaces.Middleware.IBeforeDecodeMiddleware.BeforeMessageDecodeAsync(MQContract.Interfaces.Middleware.IContext,System.String,MQContract.Messages.MessageHeader,System.String,System.String,System.ReadOnlyMemory{System.Byte})') +- [IBeforeEncodeMiddleware](#T-MQContract-Interfaces-Middleware-IBeforeEncodeMiddleware 'MQContract.Interfaces.Middleware.IBeforeEncodeMiddleware') + - [BeforeMessageEncodeAsync\`\`1(context,message,channel,messageHeader)](#M-MQContract-Interfaces-Middleware-IBeforeEncodeMiddleware-BeforeMessageEncodeAsync``1-MQContract-Interfaces-Middleware-IContext,``0,System-String,MQContract-Messages-MessageHeader- 'MQContract.Interfaces.Middleware.IBeforeEncodeMiddleware.BeforeMessageEncodeAsync``1(MQContract.Interfaces.Middleware.IContext,``0,System.String,MQContract.Messages.MessageHeader)') +- [IBeforeEncodeSpecificTypeMiddleware\`1](#T-MQContract-Interfaces-Middleware-IBeforeEncodeSpecificTypeMiddleware`1 'MQContract.Interfaces.Middleware.IBeforeEncodeSpecificTypeMiddleware`1') + - [BeforeMessageEncodeAsync(context,message,channel,messageHeader)](#M-MQContract-Interfaces-Middleware-IBeforeEncodeSpecificTypeMiddleware`1-BeforeMessageEncodeAsync-MQContract-Interfaces-Middleware-IContext,`0,System-String,MQContract-Messages-MessageHeader- 'MQContract.Interfaces.Middleware.IBeforeEncodeSpecificTypeMiddleware`1.BeforeMessageEncodeAsync(MQContract.Interfaces.Middleware.IContext,`0,System.String,MQContract.Messages.MessageHeader)') +- [IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') + - [Item](#P-MQContract-Interfaces-Middleware-IContext-Item-System-String- 'MQContract.Interfaces.Middleware.IContext.Item(System.String)') - [IContractConnection](#T-MQContract-Interfaces-IContractConnection 'MQContract.Interfaces.IContractConnection') + - [AddMetrics(meter,useInternal)](#M-MQContract-Interfaces-IContractConnection-AddMetrics-System-Diagnostics-Metrics-Meter,System-Boolean- 'MQContract.Interfaces.IContractConnection.AddMetrics(System.Diagnostics.Metrics.Meter,System.Boolean)') + - [CloseAsync()](#M-MQContract-Interfaces-IContractConnection-CloseAsync 'MQContract.Interfaces.IContractConnection.CloseAsync') + - [GetSnapshot(sent)](#M-MQContract-Interfaces-IContractConnection-GetSnapshot-System-Boolean- 'MQContract.Interfaces.IContractConnection.GetSnapshot(System.Boolean)') + - [GetSnapshot(messageType,sent)](#M-MQContract-Interfaces-IContractConnection-GetSnapshot-System-Type,System-Boolean- 'MQContract.Interfaces.IContractConnection.GetSnapshot(System.Type,System.Boolean)') + - [GetSnapshot(channel,sent)](#M-MQContract-Interfaces-IContractConnection-GetSnapshot-System-String,System-Boolean- 'MQContract.Interfaces.IContractConnection.GetSnapshot(System.String,System.Boolean)') + - [GetSnapshot\`\`1(sent)](#M-MQContract-Interfaces-IContractConnection-GetSnapshot``1-System-Boolean- 'MQContract.Interfaces.IContractConnection.GetSnapshot``1(System.Boolean)') - [PingAsync()](#M-MQContract-Interfaces-IContractConnection-PingAsync 'MQContract.Interfaces.IContractConnection.PingAsync') - - [PublishAsync\`\`1(message,channel,messageHeader,options,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-PublishAsync``1-``0,System-String,MQContract-Messages-MessageHeader,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.PublishAsync``1(``0,System.String,MQContract.Messages.MessageHeader,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync\`\`1(message,timeout,channel,messageHeader,options,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-QueryAsync``1-``0,System-Nullable{System-TimeSpan},System-String,MQContract-Messages-MessageHeader,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.QueryAsync``1(``0,System.Nullable{System.TimeSpan},System.String,MQContract.Messages.MessageHeader,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync\`\`2(message,timeout,channel,messageHeader,options,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-QueryAsync``2-``0,System-Nullable{System-TimeSpan},System-String,MQContract-Messages-MessageHeader,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.QueryAsync``2(``0,System.Nullable{System.TimeSpan},System.String,MQContract.Messages.MessageHeader,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeAsync\`\`1(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-SubscribeAsync``1-System-Func{MQContract-Interfaces-IRecievedMessage{``0},System-Threading-Tasks-Task},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Boolean,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.SubscribeAsync``1(System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Boolean,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeQueryResponseAsync\`\`2(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-SubscribeQueryResponseAsync``2-System-Func{MQContract-Interfaces-IRecievedMessage{``0},System-Threading-Tasks-Task{MQContract-Messages-QueryResponseMessage{``1}}},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Boolean,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.SubscribeQueryResponseAsync``2(System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task{MQContract.Messages.QueryResponseMessage{``1}}},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Boolean,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') + - [PublishAsync\`\`1(message,channel,messageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-PublishAsync``1-``0,System-String,MQContract-Messages-MessageHeader,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.PublishAsync``1(``0,System.String,MQContract.Messages.MessageHeader,System.Threading.CancellationToken)') + - [QueryAsync\`\`1(message,timeout,channel,responseChannel,messageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-QueryAsync``1-``0,System-Nullable{System-TimeSpan},System-String,System-String,MQContract-Messages-MessageHeader,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.QueryAsync``1(``0,System.Nullable{System.TimeSpan},System.String,System.String,MQContract.Messages.MessageHeader,System.Threading.CancellationToken)') + - [QueryAsync\`\`2(message,timeout,channel,responseChannel,messageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-QueryAsync``2-``0,System-Nullable{System-TimeSpan},System-String,System-String,MQContract-Messages-MessageHeader,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.QueryAsync``2(``0,System.Nullable{System.TimeSpan},System.String,System.String,MQContract.Messages.MessageHeader,System.Threading.CancellationToken)') + - [RegisterMiddleware\`\`1()](#M-MQContract-Interfaces-IContractConnection-RegisterMiddleware``1 'MQContract.Interfaces.IContractConnection.RegisterMiddleware``1') + - [RegisterMiddleware\`\`1(constructInstance)](#M-MQContract-Interfaces-IContractConnection-RegisterMiddleware``1-System-Func{``0}- 'MQContract.Interfaces.IContractConnection.RegisterMiddleware``1(System.Func{``0})') + - [RegisterMiddleware\`\`2()](#M-MQContract-Interfaces-IContractConnection-RegisterMiddleware``2 'MQContract.Interfaces.IContractConnection.RegisterMiddleware``2') + - [RegisterMiddleware\`\`2(constructInstance)](#M-MQContract-Interfaces-IContractConnection-RegisterMiddleware``2-System-Func{``0}- 'MQContract.Interfaces.IContractConnection.RegisterMiddleware``2(System.Func{``0})') + - [SubscribeAsync\`\`1(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-SubscribeAsync``1-System-Func{MQContract-Interfaces-IReceivedMessage{``0},System-Threading-Tasks-ValueTask},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.SubscribeAsync``1(System.Func{MQContract.Interfaces.IReceivedMessage{``0},System.Threading.Tasks.ValueTask},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Threading.CancellationToken)') + - [SubscribeAsync\`\`1(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-SubscribeAsync``1-System-Action{MQContract-Interfaces-IReceivedMessage{``0}},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.SubscribeAsync``1(System.Action{MQContract.Interfaces.IReceivedMessage{``0}},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Threading.CancellationToken)') + - [SubscribeQueryAsyncResponseAsync\`\`2(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-SubscribeQueryAsyncResponseAsync``2-System-Func{MQContract-Interfaces-IReceivedMessage{``0},System-Threading-Tasks-ValueTask{MQContract-Messages-QueryResponseMessage{``1}}},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.SubscribeQueryAsyncResponseAsync``2(System.Func{MQContract.Interfaces.IReceivedMessage{``0},System.Threading.Tasks.ValueTask{MQContract.Messages.QueryResponseMessage{``1}}},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Threading.CancellationToken)') + - [SubscribeQueryResponseAsync\`\`2(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken)](#M-MQContract-Interfaces-IContractConnection-SubscribeQueryResponseAsync``2-System-Func{MQContract-Interfaces-IReceivedMessage{``0},MQContract-Messages-QueryResponseMessage{``1}},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Threading-CancellationToken- 'MQContract.Interfaces.IContractConnection.SubscribeQueryResponseAsync``2(System.Func{MQContract.Interfaces.IReceivedMessage{``0},MQContract.Messages.QueryResponseMessage{``1}},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Threading.CancellationToken)') +- [IContractMetric](#T-MQContract-Interfaces-IContractMetric 'MQContract.Interfaces.IContractMetric') + - [MessageBytes](#P-MQContract-Interfaces-IContractMetric-MessageBytes 'MQContract.Interfaces.IContractMetric.MessageBytes') + - [MessageBytesAverage](#P-MQContract-Interfaces-IContractMetric-MessageBytesAverage 'MQContract.Interfaces.IContractMetric.MessageBytesAverage') + - [MessageBytesMax](#P-MQContract-Interfaces-IContractMetric-MessageBytesMax 'MQContract.Interfaces.IContractMetric.MessageBytesMax') + - [MessageBytesMin](#P-MQContract-Interfaces-IContractMetric-MessageBytesMin 'MQContract.Interfaces.IContractMetric.MessageBytesMin') + - [MessageConversionAverage](#P-MQContract-Interfaces-IContractMetric-MessageConversionAverage 'MQContract.Interfaces.IContractMetric.MessageConversionAverage') + - [MessageConversionDuration](#P-MQContract-Interfaces-IContractMetric-MessageConversionDuration 'MQContract.Interfaces.IContractMetric.MessageConversionDuration') + - [MessageConversionMax](#P-MQContract-Interfaces-IContractMetric-MessageConversionMax 'MQContract.Interfaces.IContractMetric.MessageConversionMax') + - [MessageConversionMin](#P-MQContract-Interfaces-IContractMetric-MessageConversionMin 'MQContract.Interfaces.IContractMetric.MessageConversionMin') + - [Messages](#P-MQContract-Interfaces-IContractMetric-Messages 'MQContract.Interfaces.IContractMetric.Messages') - [IEncodedMessage](#T-MQContract-Interfaces-Messages-IEncodedMessage 'MQContract.Interfaces.Messages.IEncodedMessage') - [Data](#P-MQContract-Interfaces-Messages-IEncodedMessage-Data 'MQContract.Interfaces.Messages.IEncodedMessage.Data') - [Header](#P-MQContract-Interfaces-Messages-IEncodedMessage-Header 'MQContract.Interfaces.Messages.IEncodedMessage.Header') - [MessageTypeID](#P-MQContract-Interfaces-Messages-IEncodedMessage-MessageTypeID 'MQContract.Interfaces.Messages.IEncodedMessage.MessageTypeID') +- [IInboxQueryableMessageServiceConnection](#T-MQContract-Interfaces-Service-IInboxQueryableMessageServiceConnection 'MQContract.Interfaces.Service.IInboxQueryableMessageServiceConnection') + - [EstablishInboxSubscriptionAsync(messageReceived,cancellationToken)](#M-MQContract-Interfaces-Service-IInboxQueryableMessageServiceConnection-EstablishInboxSubscriptionAsync-System-Action{MQContract-Messages-ReceivedInboxServiceMessage},System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IInboxQueryableMessageServiceConnection.EstablishInboxSubscriptionAsync(System.Action{MQContract.Messages.ReceivedInboxServiceMessage},System.Threading.CancellationToken)') + - [QueryAsync(message,correlationID,cancellationToken)](#M-MQContract-Interfaces-Service-IInboxQueryableMessageServiceConnection-QueryAsync-MQContract-Messages-ServiceMessage,System-Guid,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IInboxQueryableMessageServiceConnection.QueryAsync(MQContract.Messages.ServiceMessage,System.Guid,System.Threading.CancellationToken)') - [IMessageConverter\`2](#T-MQContract-Interfaces-Conversion-IMessageConverter`2 'MQContract.Interfaces.Conversion.IMessageConverter`2') - - [Convert(source)](#M-MQContract-Interfaces-Conversion-IMessageConverter`2-Convert-`0- 'MQContract.Interfaces.Conversion.IMessageConverter`2.Convert(`0)') + - [ConvertAsync(source)](#M-MQContract-Interfaces-Conversion-IMessageConverter`2-ConvertAsync-`0- 'MQContract.Interfaces.Conversion.IMessageConverter`2.ConvertAsync(`0)') - [IMessageEncoder](#T-MQContract-Interfaces-Encoding-IMessageEncoder 'MQContract.Interfaces.Encoding.IMessageEncoder') - - [Decode\`\`1(stream)](#M-MQContract-Interfaces-Encoding-IMessageEncoder-Decode``1-System-IO-Stream- 'MQContract.Interfaces.Encoding.IMessageEncoder.Decode``1(System.IO.Stream)') - - [Encode\`\`1(message)](#M-MQContract-Interfaces-Encoding-IMessageEncoder-Encode``1-``0- 'MQContract.Interfaces.Encoding.IMessageEncoder.Encode``1(``0)') + - [DecodeAsync\`\`1(stream)](#M-MQContract-Interfaces-Encoding-IMessageEncoder-DecodeAsync``1-System-IO-Stream- 'MQContract.Interfaces.Encoding.IMessageEncoder.DecodeAsync``1(System.IO.Stream)') + - [EncodeAsync\`\`1(message)](#M-MQContract-Interfaces-Encoding-IMessageEncoder-EncodeAsync``1-``0- 'MQContract.Interfaces.Encoding.IMessageEncoder.EncodeAsync``1(``0)') - [IMessageEncryptor](#T-MQContract-Interfaces-Encrypting-IMessageEncryptor 'MQContract.Interfaces.Encrypting.IMessageEncryptor') - - [Decrypt(stream,headers)](#M-MQContract-Interfaces-Encrypting-IMessageEncryptor-Decrypt-System-IO-Stream,MQContract-Messages-MessageHeader- 'MQContract.Interfaces.Encrypting.IMessageEncryptor.Decrypt(System.IO.Stream,MQContract.Messages.MessageHeader)') - - [Encrypt(data,headers)](#M-MQContract-Interfaces-Encrypting-IMessageEncryptor-Encrypt-System-Byte[],System-Collections-Generic-Dictionary{System-String,System-String}@- 'MQContract.Interfaces.Encrypting.IMessageEncryptor.Encrypt(System.Byte[],System.Collections.Generic.Dictionary{System.String,System.String}@)') + - [DecryptAsync(stream,headers)](#M-MQContract-Interfaces-Encrypting-IMessageEncryptor-DecryptAsync-System-IO-Stream,MQContract-Messages-MessageHeader- 'MQContract.Interfaces.Encrypting.IMessageEncryptor.DecryptAsync(System.IO.Stream,MQContract.Messages.MessageHeader)') + - [EncryptAsync(data,headers)](#M-MQContract-Interfaces-Encrypting-IMessageEncryptor-EncryptAsync-System-Byte[],System-Collections-Generic-Dictionary{System-String,System-String}@- 'MQContract.Interfaces.Encrypting.IMessageEncryptor.EncryptAsync(System.Byte[],System.Collections.Generic.Dictionary{System.String,System.String}@)') - [IMessageServiceConnection](#T-MQContract-Interfaces-Service-IMessageServiceConnection 'MQContract.Interfaces.Service.IMessageServiceConnection') - - [DefaultTimout](#P-MQContract-Interfaces-Service-IMessageServiceConnection-DefaultTimout 'MQContract.Interfaces.Service.IMessageServiceConnection.DefaultTimout') - [MaxMessageBodySize](#P-MQContract-Interfaces-Service-IMessageServiceConnection-MaxMessageBodySize 'MQContract.Interfaces.Service.IMessageServiceConnection.MaxMessageBodySize') - - [PingAsync()](#M-MQContract-Interfaces-Service-IMessageServiceConnection-PingAsync 'MQContract.Interfaces.Service.IMessageServiceConnection.PingAsync') - - [PublishAsync(message,options,cancellationToken)](#M-MQContract-Interfaces-Service-IMessageServiceConnection-PublishAsync-MQContract-Messages-ServiceMessage,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IMessageServiceConnection.PublishAsync(MQContract.Messages.ServiceMessage,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync(message,timeout,options,cancellationToken)](#M-MQContract-Interfaces-Service-IMessageServiceConnection-QueryAsync-MQContract-Messages-ServiceMessage,System-TimeSpan,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IMessageServiceConnection.QueryAsync(MQContract.Messages.ServiceMessage,System.TimeSpan,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-Interfaces-Service-IMessageServiceConnection-SubscribeAsync-System-Action{MQContract-Messages-RecievedServiceMessage},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IMessageServiceConnection.SubscribeAsync(System.Action{MQContract.Messages.RecievedServiceMessage},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-Interfaces-Service-IMessageServiceConnection-SubscribeQueryAsync-System-Func{MQContract-Messages-RecievedServiceMessage,System-Threading-Tasks-Task{MQContract-Messages-ServiceMessage}},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IMessageServiceConnection.SubscribeQueryAsync(System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') + - [CloseAsync()](#M-MQContract-Interfaces-Service-IMessageServiceConnection-CloseAsync 'MQContract.Interfaces.Service.IMessageServiceConnection.CloseAsync') + - [PublishAsync(message,cancellationToken)](#M-MQContract-Interfaces-Service-IMessageServiceConnection-PublishAsync-MQContract-Messages-ServiceMessage,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IMessageServiceConnection.PublishAsync(MQContract.Messages.ServiceMessage,System.Threading.CancellationToken)') + - [SubscribeAsync(messageReceived,errorReceived,channel,group,cancellationToken)](#M-MQContract-Interfaces-Service-IMessageServiceConnection-SubscribeAsync-System-Action{MQContract-Messages-ReceivedServiceMessage},System-Action{System-Exception},System-String,System-String,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IMessageServiceConnection.SubscribeAsync(System.Action{MQContract.Messages.ReceivedServiceMessage},System.Action{System.Exception},System.String,System.String,System.Threading.CancellationToken)') - [IMessageTypeEncoder\`1](#T-MQContract-Interfaces-Encoding-IMessageTypeEncoder`1 'MQContract.Interfaces.Encoding.IMessageTypeEncoder`1') - - [Decode(stream)](#M-MQContract-Interfaces-Encoding-IMessageTypeEncoder`1-Decode-System-IO-Stream- 'MQContract.Interfaces.Encoding.IMessageTypeEncoder`1.Decode(System.IO.Stream)') - - [Encode(message)](#M-MQContract-Interfaces-Encoding-IMessageTypeEncoder`1-Encode-`0- 'MQContract.Interfaces.Encoding.IMessageTypeEncoder`1.Encode(`0)') + - [DecodeAsync(stream)](#M-MQContract-Interfaces-Encoding-IMessageTypeEncoder`1-DecodeAsync-System-IO-Stream- 'MQContract.Interfaces.Encoding.IMessageTypeEncoder`1.DecodeAsync(System.IO.Stream)') + - [EncodeAsync(message)](#M-MQContract-Interfaces-Encoding-IMessageTypeEncoder`1-EncodeAsync-`0- 'MQContract.Interfaces.Encoding.IMessageTypeEncoder`1.EncodeAsync(`0)') - [IMessageTypeEncryptor\`1](#T-MQContract-Interfaces-Encrypting-IMessageTypeEncryptor`1 'MQContract.Interfaces.Encrypting.IMessageTypeEncryptor`1') -- [IRecievedMessage\`1](#T-MQContract-Interfaces-IRecievedMessage`1 'MQContract.Interfaces.IRecievedMessage`1') - - [Headers](#P-MQContract-Interfaces-IRecievedMessage`1-Headers 'MQContract.Interfaces.IRecievedMessage`1.Headers') - - [ID](#P-MQContract-Interfaces-IRecievedMessage`1-ID 'MQContract.Interfaces.IRecievedMessage`1.ID') - - [Message](#P-MQContract-Interfaces-IRecievedMessage`1-Message 'MQContract.Interfaces.IRecievedMessage`1.Message') - - [ProcessedTimestamp](#P-MQContract-Interfaces-IRecievedMessage`1-ProcessedTimestamp 'MQContract.Interfaces.IRecievedMessage`1.ProcessedTimestamp') - - [RecievedTimestamp](#P-MQContract-Interfaces-IRecievedMessage`1-RecievedTimestamp 'MQContract.Interfaces.IRecievedMessage`1.RecievedTimestamp') -- [IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') +- [IMiddleware](#T-MQContract-Interfaces-Middleware-IMiddleware 'MQContract.Interfaces.Middleware.IMiddleware') +- [IPingableMessageServiceConnection](#T-MQContract-Interfaces-Service-IPingableMessageServiceConnection 'MQContract.Interfaces.Service.IPingableMessageServiceConnection') + - [PingAsync()](#M-MQContract-Interfaces-Service-IPingableMessageServiceConnection-PingAsync 'MQContract.Interfaces.Service.IPingableMessageServiceConnection.PingAsync') +- [IQueryResponseMessageServiceConnection](#T-MQContract-Interfaces-Service-IQueryResponseMessageServiceConnection 'MQContract.Interfaces.Service.IQueryResponseMessageServiceConnection') + - [QueryAsync(message,timeout,cancellationToken)](#M-MQContract-Interfaces-Service-IQueryResponseMessageServiceConnection-QueryAsync-MQContract-Messages-ServiceMessage,System-TimeSpan,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IQueryResponseMessageServiceConnection.QueryAsync(MQContract.Messages.ServiceMessage,System.TimeSpan,System.Threading.CancellationToken)') +- [IQueryableMessageServiceConnection](#T-MQContract-Interfaces-Service-IQueryableMessageServiceConnection 'MQContract.Interfaces.Service.IQueryableMessageServiceConnection') + - [DefaultTimeout](#P-MQContract-Interfaces-Service-IQueryableMessageServiceConnection-DefaultTimeout 'MQContract.Interfaces.Service.IQueryableMessageServiceConnection.DefaultTimeout') + - [SubscribeQueryAsync(messageReceived,errorReceived,channel,group,cancellationToken)](#M-MQContract-Interfaces-Service-IQueryableMessageServiceConnection-SubscribeQueryAsync-System-Func{MQContract-Messages-ReceivedServiceMessage,System-Threading-Tasks-ValueTask{MQContract-Messages-ServiceMessage}},System-Action{System-Exception},System-String,System-String,System-Threading-CancellationToken- 'MQContract.Interfaces.Service.IQueryableMessageServiceConnection.SubscribeQueryAsync(System.Func{MQContract.Messages.ReceivedServiceMessage,System.Threading.Tasks.ValueTask{MQContract.Messages.ServiceMessage}},System.Action{System.Exception},System.String,System.String,System.Threading.CancellationToken)') +- [IReceivedMessage\`1](#T-MQContract-Interfaces-IReceivedMessage`1 'MQContract.Interfaces.IReceivedMessage`1') + - [Headers](#P-MQContract-Interfaces-IReceivedMessage`1-Headers 'MQContract.Interfaces.IReceivedMessage`1.Headers') + - [ID](#P-MQContract-Interfaces-IReceivedMessage`1-ID 'MQContract.Interfaces.IReceivedMessage`1.ID') + - [Message](#P-MQContract-Interfaces-IReceivedMessage`1-Message 'MQContract.Interfaces.IReceivedMessage`1.Message') + - [ProcessedTimestamp](#P-MQContract-Interfaces-IReceivedMessage`1-ProcessedTimestamp 'MQContract.Interfaces.IReceivedMessage`1.ProcessedTimestamp') + - [ReceivedTimestamp](#P-MQContract-Interfaces-IReceivedMessage`1-ReceivedTimestamp 'MQContract.Interfaces.IReceivedMessage`1.ReceivedTimestamp') - [IServiceSubscription](#T-MQContract-Interfaces-Service-IServiceSubscription 'MQContract.Interfaces.Service.IServiceSubscription') - [EndAsync()](#M-MQContract-Interfaces-Service-IServiceSubscription-EndAsync 'MQContract.Interfaces.Service.IServiceSubscription.EndAsync') +- [ISpecificTypeMiddleware\`1](#T-MQContract-Interfaces-Middleware-ISpecificTypeMiddleware`1 'MQContract.Interfaces.Middleware.ISpecificTypeMiddleware`1') - [ISubscription](#T-MQContract-Interfaces-ISubscription 'MQContract.Interfaces.ISubscription') - [EndAsync()](#M-MQContract-Interfaces-ISubscription-EndAsync 'MQContract.Interfaces.ISubscription.EndAsync') -- [InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') - - [ThrowIfNotNullAndNotOfType(options,expectedTypes)](#M-MQContract-InvalidChannelOptionsTypeException-ThrowIfNotNullAndNotOfType-MQContract-Interfaces-Service-IServiceChannelOptions,System-Collections-Generic-IEnumerable{System-Type}- 'MQContract.InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(MQContract.Interfaces.Service.IServiceChannelOptions,System.Collections.Generic.IEnumerable{System.Type})') - - [ThrowIfNotNullAndNotOfType\`\`1(options)](#M-MQContract-InvalidChannelOptionsTypeException-ThrowIfNotNullAndNotOfType``1-MQContract-Interfaces-Service-IServiceChannelOptions- 'MQContract.InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType``1(MQContract.Interfaces.Service.IServiceChannelOptions)') - [MessageChannelAttribute](#T-MQContract-Attributes-MessageChannelAttribute 'MQContract.Attributes.MessageChannelAttribute') - [#ctor(name)](#M-MQContract-Attributes-MessageChannelAttribute-#ctor-System-String- 'MQContract.Attributes.MessageChannelAttribute.#ctor(System.String)') - [Name](#P-MQContract-Attributes-MessageChannelAttribute-Name 'MQContract.Attributes.MessageChannelAttribute.Name') @@ -62,17 +103,19 @@ - [Value](#P-MQContract-Attributes-MessageNameAttribute-Value 'MQContract.Attributes.MessageNameAttribute.Value') - [MessageResponseTimeoutAttribute](#T-MQContract-Attributes-MessageResponseTimeoutAttribute 'MQContract.Attributes.MessageResponseTimeoutAttribute') - [#ctor(value)](#M-MQContract-Attributes-MessageResponseTimeoutAttribute-#ctor-System-Int32- 'MQContract.Attributes.MessageResponseTimeoutAttribute.#ctor(System.Int32)') + - [TimeSpanValue](#P-MQContract-Attributes-MessageResponseTimeoutAttribute-TimeSpanValue 'MQContract.Attributes.MessageResponseTimeoutAttribute.TimeSpanValue') - [Value](#P-MQContract-Attributes-MessageResponseTimeoutAttribute-Value 'MQContract.Attributes.MessageResponseTimeoutAttribute.Value') - [MessageVersionAttribute](#T-MQContract-Attributes-MessageVersionAttribute 'MQContract.Attributes.MessageVersionAttribute') - [#ctor(version)](#M-MQContract-Attributes-MessageVersionAttribute-#ctor-System-String- 'MQContract.Attributes.MessageVersionAttribute.#ctor(System.String)') - [Version](#P-MQContract-Attributes-MessageVersionAttribute-Version 'MQContract.Attributes.MessageVersionAttribute.Version') -- [NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') - - [ThrowIfNotNull(options)](#M-MQContract-NoChannelOptionsAvailableException-ThrowIfNotNull-MQContract-Interfaces-Service-IServiceChannelOptions- 'MQContract.NoChannelOptionsAvailableException.ThrowIfNotNull(MQContract.Interfaces.Service.IServiceChannelOptions)') - [PingResult](#T-MQContract-Messages-PingResult 'MQContract.Messages.PingResult') - [#ctor(Host,Version,ResponseTime)](#M-MQContract-Messages-PingResult-#ctor-System-String,System-String,System-TimeSpan- 'MQContract.Messages.PingResult.#ctor(System.String,System.String,System.TimeSpan)') - [Host](#P-MQContract-Messages-PingResult-Host 'MQContract.Messages.PingResult.Host') - [ResponseTime](#P-MQContract-Messages-PingResult-ResponseTime 'MQContract.Messages.PingResult.ResponseTime') - [Version](#P-MQContract-Messages-PingResult-Version 'MQContract.Messages.PingResult.Version') +- [QueryResponseChannelAttribute](#T-MQContract-Attributes-QueryResponseChannelAttribute 'MQContract.Attributes.QueryResponseChannelAttribute') + - [#ctor(name)](#M-MQContract-Attributes-QueryResponseChannelAttribute-#ctor-System-String- 'MQContract.Attributes.QueryResponseChannelAttribute.#ctor(System.String)') + - [Name](#P-MQContract-Attributes-QueryResponseChannelAttribute-Name 'MQContract.Attributes.QueryResponseChannelAttribute.Name') - [QueryResponseMessage\`1](#T-MQContract-Messages-QueryResponseMessage`1 'MQContract.Messages.QueryResponseMessage`1') - [#ctor(Message,Headers)](#M-MQContract-Messages-QueryResponseMessage`1-#ctor-`0,System-Collections-Generic-Dictionary{System-String,System-String}- 'MQContract.Messages.QueryResponseMessage`1.#ctor(`0,System.Collections.Generic.Dictionary{System.String,System.String})') - [Headers](#P-MQContract-Messages-QueryResponseMessage`1-Headers 'MQContract.Messages.QueryResponseMessage`1.Headers') @@ -84,9 +127,13 @@ - [#ctor(ID,Header,Result,Error)](#M-MQContract-Messages-QueryResult`1-#ctor-System-String,MQContract-Messages-MessageHeader,`0,System-String- 'MQContract.Messages.QueryResult`1.#ctor(System.String,MQContract.Messages.MessageHeader,`0,System.String)') - [Header](#P-MQContract-Messages-QueryResult`1-Header 'MQContract.Messages.QueryResult`1.Header') - [Result](#P-MQContract-Messages-QueryResult`1-Result 'MQContract.Messages.QueryResult`1.Result') -- [RecievedServiceMessage](#T-MQContract-Messages-RecievedServiceMessage 'MQContract.Messages.RecievedServiceMessage') - - [#ctor(ID,MessageTypeID,Channel,Header,Data)](#M-MQContract-Messages-RecievedServiceMessage-#ctor-System-String,System-String,System-String,MQContract-Messages-MessageHeader,System-ReadOnlyMemory{System-Byte}- 'MQContract.Messages.RecievedServiceMessage.#ctor(System.String,System.String,System.String,MQContract.Messages.MessageHeader,System.ReadOnlyMemory{System.Byte})') - - [RecievedTimestamp](#P-MQContract-Messages-RecievedServiceMessage-RecievedTimestamp 'MQContract.Messages.RecievedServiceMessage.RecievedTimestamp') +- [ReceivedInboxServiceMessage](#T-MQContract-Messages-ReceivedInboxServiceMessage 'MQContract.Messages.ReceivedInboxServiceMessage') + - [#ctor(ID,MessageTypeID,Channel,Header,CorrelationID,Data,Acknowledge)](#M-MQContract-Messages-ReceivedInboxServiceMessage-#ctor-System-String,System-String,System-String,MQContract-Messages-MessageHeader,System-Guid,System-ReadOnlyMemory{System-Byte},System-Func{System-Threading-Tasks-ValueTask}- 'MQContract.Messages.ReceivedInboxServiceMessage.#ctor(System.String,System.String,System.String,MQContract.Messages.MessageHeader,System.Guid,System.ReadOnlyMemory{System.Byte},System.Func{System.Threading.Tasks.ValueTask})') + - [CorrelationID](#P-MQContract-Messages-ReceivedInboxServiceMessage-CorrelationID 'MQContract.Messages.ReceivedInboxServiceMessage.CorrelationID') +- [ReceivedServiceMessage](#T-MQContract-Messages-ReceivedServiceMessage 'MQContract.Messages.ReceivedServiceMessage') + - [#ctor(ID,MessageTypeID,Channel,Header,Data,Acknowledge)](#M-MQContract-Messages-ReceivedServiceMessage-#ctor-System-String,System-String,System-String,MQContract-Messages-MessageHeader,System-ReadOnlyMemory{System-Byte},System-Func{System-Threading-Tasks-ValueTask}- 'MQContract.Messages.ReceivedServiceMessage.#ctor(System.String,System.String,System.String,MQContract.Messages.MessageHeader,System.ReadOnlyMemory{System.Byte},System.Func{System.Threading.Tasks.ValueTask})') + - [Acknowledge](#P-MQContract-Messages-ReceivedServiceMessage-Acknowledge 'MQContract.Messages.ReceivedServiceMessage.Acknowledge') + - [ReceivedTimestamp](#P-MQContract-Messages-ReceivedServiceMessage-ReceivedTimestamp 'MQContract.Messages.ReceivedServiceMessage.ReceivedTimestamp') - [ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') - [#ctor(ID,MessageTypeID,Channel,Header,Data)](#M-MQContract-Messages-ServiceMessage-#ctor-System-String,System-String,System-String,MQContract-Messages-MessageHeader,System-ReadOnlyMemory{System-Byte}- 'MQContract.Messages.ServiceMessage.#ctor(System.String,System.String,System.String,MQContract.Messages.MessageHeader,System.ReadOnlyMemory{System.Byte})') - [Channel](#P-MQContract-Messages-ServiceMessage-Channel 'MQContract.Messages.ServiceMessage.Channel') @@ -106,6 +153,237 @@ - [ID](#P-MQContract-Messages-TransmissionResult-ID 'MQContract.Messages.TransmissionResult.ID') - [IsError](#P-MQContract-Messages-TransmissionResult-IsError 'MQContract.Messages.TransmissionResult.IsError') + +## IAfterDecodeMiddleware `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This interface represents a Middleware to execute after a Message has been decoded from a ServiceMessage to the expected Class + + +### AfterMessageDecodeAsync\`\`1(context,message,ID,messageHeader,receivedTimestamp,processedTimeStamp) `method` + +##### Summary + +This is the method invoked as part of the Middleware processing during message decoding + +##### Returns + +The message and header to allow for changes if desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| context | [MQContract.Interfaces.Middleware.IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') | A shared context that exists from the start of this decode process instance | +| message | [\`\`0](#T-``0 '``0') | The class message | +| ID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The id of the message | +| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The headers from the message | +| receivedTimestamp | [System.DateTime](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.DateTime 'System.DateTime') | The timestamp of when the message was recieved | +| processedTimeStamp | [System.DateTime](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.DateTime 'System.DateTime') | The timestamp of when the message was decoded into a Class | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | This will be the type of the Message that was decoded | + + +## IAfterDecodeSpecificTypeMiddleware\`1 `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This interface represents a Middleware to execute after a Message of the given type T has been decoded from a ServiceMessage to the expected Class + + +### AfterMessageDecodeAsync(context,message,ID,messageHeader,receivedTimestamp,processedTimeStamp) `method` + +##### Summary + +This is the method invoked as part of the Middleware processing during message decoding + +##### Returns + +The message and header to allow for changes if desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| context | [MQContract.Interfaces.Middleware.IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') | A shared context that exists from the start of this decode process instance | +| message | [\`0](#T-`0 '`0') | The class message | +| ID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The id of the message | +| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The headers from the message | +| receivedTimestamp | [System.DateTime](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.DateTime 'System.DateTime') | The timestamp of when the message was recieved | +| processedTimeStamp | [System.DateTime](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.DateTime 'System.DateTime') | The timestamp of when the message was decoded into a Class | + + +## IAfterEncodeMiddleware `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This interface represents a Middleware to execute after a Message has been encoded to a ServiceMessage from the supplied Class + + +### AfterMessageEncodeAsync(messageType,context,message) `method` + +##### Summary + +This is the method invoked as part of the Middleware processing during message encoding + +##### Returns + +The message to allow for changes if desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| messageType | [System.Type](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Type 'System.Type') | The class of the message type that was encoded | +| context | [MQContract.Interfaces.Middleware.IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') | A shared context that exists from the start of this encode process instance | +| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The resulting encoded message | + + +## IBeforeDecodeMiddleware `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This interface represents a Middleware to execute before decoding a ServiceMessage + + +### BeforeMessageDecodeAsync(context,id,messageHeader,messageTypeID,messageChannel,data) `method` + +##### Summary + +This is the method invoked as part of the Middleware processing prior to the message decoding + +##### Returns + +The message header and data to allow for changes if desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| context | [MQContract.Interfaces.Middleware.IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') | A shared context that exists from the start of this decode process instance | +| id | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The id of the message | +| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The headers from the message | +| messageTypeID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The message type id | +| messageChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel the message was recieved on | +| data | [System.ReadOnlyMemory{System.Byte}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.ReadOnlyMemory 'System.ReadOnlyMemory{System.Byte}') | The data of the message | + + +## IBeforeEncodeMiddleware `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This interface represents a Middleware to execute Before a message is encoded + + +### BeforeMessageEncodeAsync\`\`1(context,message,channel,messageHeader) `method` + +##### Summary + +This is the method invoked as part of the Middle Ware processing during message encoding + +##### Returns + +The message, channel and header to allow for changes if desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| context | [MQContract.Interfaces.Middleware.IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') | A shared context that exists from the start of this encoding instance | +| message | [\`\`0](#T-``0 '``0') | The message being encoded | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel this message was requested to transmit to | +| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The message headers being supplied | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of message being processed | + + +## IBeforeEncodeSpecificTypeMiddleware\`1 `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This interface represents a Middleware to execute Before a specific message type is encoded + + +### BeforeMessageEncodeAsync(context,message,channel,messageHeader) `method` + +##### Summary + +This is the method invoked as part of the Middle Ware processing during message encoding + +##### Returns + +The message, channel and header to allow for changes if desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| context | [MQContract.Interfaces.Middleware.IContext](#T-MQContract-Interfaces-Middleware-IContext 'MQContract.Interfaces.Middleware.IContext') | A shared context that exists from the start of this encoding instance | +| message | [\`0](#T-`0 '`0') | The message being encoded | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel this message was requested to transmit to | +| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The message headers being supplied | + + +## IContext `type` + +##### Namespace + +MQContract.Interfaces.Middleware + +##### Summary + +This is used to represent a Context for the middleware calls to use that exists from the start to the end of the message conversion process + + +### Item `property` + +##### Summary + +Used to store and retreive values from the context during the conversion process. + +##### Returns + +The value if it exists in the context + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| key | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The unique key to use | + ## IContractConnection `type` @@ -117,6 +395,137 @@ MQContract.Interfaces This interface represents the Core class for the MQContract system, IE the ContractConnection + +### AddMetrics(meter,useInternal) `method` + +##### Summary + +Called to activate the metrics tracking middleware for this connection instance + +##### Returns + +The Contract Connection instance to allow chaining calls + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| meter | [System.Diagnostics.Metrics.Meter](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Diagnostics.Metrics.Meter 'System.Diagnostics.Metrics.Meter') | The Meter item to create all system metrics against | +| useInternal | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Indicates if the internal metrics collector should be used | + +##### Remarks + +For the Meter metrics, all durations are in ms and the following values and patterns will apply: +mqcontract.messages.sent.count = count of messages sent (Counter) +mqcontract.messages.sent.bytes = count of bytes sent (message data) (Counter) +mqcontract.messages.received.count = count of messages received (Counter) +mqcontract.messages.received.bytes = count of bytes received (message data) (Counter) +mqcontract.messages.encodingduration = milliseconds to encode messages (Histogram) +mqcontract.messages.decodingduration = milliseconds to decode messages (Histogram) +mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.sent.count = count of messages sent of a given type (Counter) +mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.sent.bytes = count of bytes sent (message data) of a given type (Counter) +mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.received.count = count of messages received of a given type (Counter) +mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.received.bytes = count of bytes received (message data) of a given type (Counter) +mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.encodingduration = milliseconds to encode messages of a given type (Histogram) +mqcontract.types.{MessageTypeName}.{MessageVersion(_ instead of .)}.decodingduration = milliseconds to decode messages of a given type (Histogram) +mqcontract.channels.{Channel}.sent.count = count of messages sent for a given channel (Counter) +mqcontract.channels.{Channel}.sent.bytes = count of bytes sent (message data) for a given channel (Counter) +mqcontract.channels.{Channel}.received.count = count of messages received for a given channel (Counter) +mqcontract.channels.{Channel}.received.bytes = count of bytes received (message data) for a given channel (Counter) +mqcontract.channels.{Channel}.encodingduration = milliseconds to encode messages for a given channel (Histogram) +mqcontract.channels.{Channel}.decodingduration = milliseconds to decode messages for a given channel (Histogram) + + +### CloseAsync() `method` + +##### Summary + +Called to close off the contract connection and close it's underlying service connection + +##### Returns + +A task for the closure of the connection + +##### Parameters + +This method has no parameters. + + +### GetSnapshot(sent) `method` + +##### Summary + +Called to get a snapshot of the current global metrics. Will return null if internal metrics are not enabled. + +##### Returns + +A record of the current metric snapshot or null if not available + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sent | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | true when the sent metrics are desired, false when received are desired | + + +### GetSnapshot(messageType,sent) `method` + +##### Summary + +Called to get a snapshot of the metrics for a given message type. Will return null if internal metrics are not enabled. + +##### Returns + +A record of the current metric snapshot or null if not available + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| messageType | [System.Type](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Type 'System.Type') | The type of message to look for | +| sent | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | true when the sent metrics are desired, false when received are desired | + + +### GetSnapshot(channel,sent) `method` + +##### Summary + +Called to get a snapshot of the metrics for a given message channel. Will return null if internal metrics are not enabled. + +##### Returns + +A record of the current metric snapshot or null if not available + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel to look for | +| sent | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | true when the sent metrics are desired, false when received are desired | + + +### GetSnapshot\`\`1(sent) `method` + +##### Summary + +Called to get a snapshot of the metrics for a given message type. Will return null if internal metrics are not enabled. + +##### Returns + +A record of the current metric snapshot or null if not available + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sent | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | true when the sent metrics are desired, false when received are desired | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of message to look for | + ### PingAsync() `method` @@ -132,8 +541,8 @@ Called to Ping the underlying system to obtain both information and ensure it is This method has no parameters. - -### PublishAsync\`\`1(message,channel,messageHeader,options,cancellationToken) `method` + +### PublishAsync\`\`1(message,channel,messageHeader,cancellationToken) `method` ##### Summary @@ -150,7 +559,6 @@ A result indicating the tranmission results | message | [\`\`0](#T-``0 '``0') | The message to send | | channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | | messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The headers to pass along with the message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Any required Service Channel Options that will be passed down to the service Connection | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | ##### Generic Types @@ -159,8 +567,8 @@ A result indicating the tranmission results | ---- | ----------- | | T | The type of message to send | - -### QueryAsync\`\`1(message,timeout,channel,messageHeader,options,cancellationToken) `method` + +### QueryAsync\`\`1(message,timeout,channel,responseChannel,messageHeader,cancellationToken) `method` ##### Summary @@ -176,10 +584,11 @@ A result indicating the success or failure as well as the returned message | Name | Type | Description | | ---- | ---- | ----------- | | message | [\`\`0](#T-``0 '``0') | The message to send | -| timeout | [System.Nullable{System.TimeSpan}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Nullable 'System.Nullable{System.TimeSpan}') | The allowed timeout prior to a response being recieved | +| timeout | [System.Nullable{System.TimeSpan}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Nullable 'System.Nullable{System.TimeSpan}') | The allowed timeout prior to a response being received | | channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | +| responseChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use for the response. The preferred method is using the QueryResponseChannelAttribute on the class. This is +only used when the underlying connection does not support a QueryResponse style messaging. | | messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The headers to pass along with the message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Any required Service Channel Options that will be passed down to the service Connection | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | ##### Generic Types @@ -188,8 +597,8 @@ A result indicating the success or failure as well as the returned message | ---- | ----------- | | Q | The type of message to send for the query | - -### QueryAsync\`\`2(message,timeout,channel,messageHeader,options,cancellationToken) `method` + +### QueryAsync\`\`2(message,timeout,channel,responseChannel,messageHeader,cancellationToken) `method` ##### Summary @@ -204,10 +613,11 @@ A result indicating the success or failure as well as the returned message | Name | Type | Description | | ---- | ---- | ----------- | | message | [\`\`0](#T-``0 '``0') | The message to send | -| timeout | [System.Nullable{System.TimeSpan}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Nullable 'System.Nullable{System.TimeSpan}') | The allowed timeout prior to a response being recieved | +| timeout | [System.Nullable{System.TimeSpan}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Nullable 'System.Nullable{System.TimeSpan}') | The allowed timeout prior to a response being received | | channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | +| responseChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use for the response. The preferred method is using the QueryResponseChannelAttribute on the class. This is +only used when the underlying connection does not support a QueryResponse style messaging. | | messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The headers to pass along with the message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Any required Service Channel Options that will be passed down to the service Connection | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | ##### Generic Types @@ -217,12 +627,102 @@ A result indicating the success or failure as well as the returned message | Q | The type of message to send for the query | | R | The type of message to expect back for the response | - -### SubscribeAsync\`\`1(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken) `method` + +### RegisterMiddleware\`\`1() `method` ##### Summary -Called to create a subscription into the underlying service Pub/Sub style +Register a middleware of a given type T to be used by the contract connection + +##### Returns + +The Contract Connection instance to allow chaining calls + +##### Parameters + +This method has no parameters. + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of middle ware to register, it must implement IBeforeDecodeMiddleware or IBeforeEncodeMiddleware or IAfterDecodeMiddleware or IAfterEncodeMiddleware | + + +### RegisterMiddleware\`\`1(constructInstance) `method` + +##### Summary + +Register a middleware of a given type T to be used by the contract connection + +##### Returns + +The Contract Connection instance to allow chaining calls + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| constructInstance | [System.Func{\`\`0}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{``0}') | Callback to create the instance | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of middle ware to register, it must implement IBeforeDecodeMiddleware or IBeforeEncodeMiddleware or IAfterDecodeMiddleware or IAfterEncodeMiddleware | + + +### RegisterMiddleware\`\`2() `method` + +##### Summary + +Register a middleware of a given type T to be used by the contract connection + +##### Returns + +The Contract Connection instance to allow chaining calls + +##### Parameters + +This method has no parameters. + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of middle ware to register, it must implement IBeforeEncodeSpecificTypeMiddleware or IAfterDecodeSpecificTypeMiddleware | +| M | The message type that this middleware is specifically called for | + + +### RegisterMiddleware\`\`2(constructInstance) `method` + +##### Summary + +Register a middleware of a given type T to be used by the contract connection + +##### Returns + +The Contract Connection instance to allow chaining calls + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| constructInstance | [System.Func{\`\`0}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{``0}') | Callback to create the instance | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of middle ware to register, it must implement IBeforeEncodeSpecificTypeMiddleware or IAfterDecodeSpecificTypeMiddleware | +| M | The message type that this middleware is specifically called for | + + +### SubscribeAsync\`\`1(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken) `method` + +##### Summary + +Called to create a subscription into the underlying service Pub/Sub style and have the messages processed asynchronously ##### Returns @@ -232,13 +732,11 @@ A subscription instance that can be ended when desired | Name | Type | Description | | ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Interfaces.IRecievedMessage{\`\`0},System.Threading.Tasks.Task}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task}') | The callback invoked when a new message is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an error occurs | +| messageReceived | [System.Func{MQContract.Interfaces.IReceivedMessage{\`\`0},System.Threading.Tasks.ValueTask}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IReceivedMessage{``0},System.Threading.Tasks.ValueTask}') | The callback invoked when a new message is received | +| errorReceived | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an error occurs | | channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | | group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The subscription group if desired (typically used when multiple instances of the same system are running) | | ignoreMessageHeader | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class | -| synchronous | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Inddicates if the callbacks for a recieved message should be called synchronously or asynchronously | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Any required Service Channel Options that will be passed down to the service Connection | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | ##### Generic Types @@ -247,12 +745,12 @@ A subscription instance that can be ended when desired | ---- | ----------- | | T | The type of message to listen for | - -### SubscribeQueryResponseAsync\`\`2(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken) `method` + +### SubscribeAsync\`\`1(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken) `method` ##### Summary -Called to create a subscription into the underlying service Query/Reponse style +Called to create a subscription into the underlying service Pub/Sub style and have the messages processed syncrhonously ##### Returns @@ -262,13 +760,39 @@ A subscription instance that can be ended when desired | Name | Type | Description | | ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Interfaces.IRecievedMessage{\`\`0},System.Threading.Tasks.Task{MQContract.Messages.QueryResponseMessage{\`\`1}}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task{MQContract.Messages.QueryResponseMessage{``1}}}') | The callback invoked when a new message is recieved expecting a response of the type response | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback invoked when an error occurs. | +| messageReceived | [System.Action{MQContract.Interfaces.IReceivedMessage{\`\`0}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Interfaces.IReceivedMessage{``0}}') | The callback invoked when a new message is received | +| errorReceived | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an error occurs | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | +| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The subscription group if desired (typically used when multiple instances of the same system are running) | +| ignoreMessageHeader | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class | +| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The type of message to listen for | + + +### SubscribeQueryAsyncResponseAsync\`\`2(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken) `method` + +##### Summary + +Called to create a subscription into the underlying service Query/Reponse style and have the messages processed asynchronously + +##### Returns + +A subscription instance that can be ended when desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| messageReceived | [System.Func{MQContract.Interfaces.IReceivedMessage{\`\`0},System.Threading.Tasks.ValueTask{MQContract.Messages.QueryResponseMessage{\`\`1}}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IReceivedMessage{``0},System.Threading.Tasks.ValueTask{MQContract.Messages.QueryResponseMessage{``1}}}') | The callback invoked when a new message is received expecting a response of the type response | +| errorReceived | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback invoked when an error occurs. | | channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | | group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The subscription group if desired (typically used when multiple instances of the same system are running) | | ignoreMessageHeader | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class | -| synchronous | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Inddicates if the callbacks for a recieved message should be called synchronously or asynchronously | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Any required Service Channel Options that will be passed down to the service Connection | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | ##### Generic Types @@ -278,6 +802,112 @@ A subscription instance that can be ended when desired | Q | The type of message to listen for | | R | The type of message to respond with | + +### SubscribeQueryResponseAsync\`\`2(messageReceived,errorReceived,channel,group,ignoreMessageHeader,cancellationToken) `method` + +##### Summary + +Called to create a subscription into the underlying service Query/Reponse style and have the messages processed synchronously + +##### Returns + +A subscription instance that can be ended when desired + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| messageReceived | [System.Func{MQContract.Interfaces.IReceivedMessage{\`\`0},MQContract.Messages.QueryResponseMessage{\`\`1}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IReceivedMessage{``0},MQContract.Messages.QueryResponseMessage{``1}}') | The callback invoked when a new message is received expecting a response of the type response | +| errorReceived | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback invoked when an error occurs. | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Specifies the message channel to use. The prefered method is using the MessageChannelAttribute on the class. | +| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The subscription group if desired (typically used when multiple instances of the same system are running) | +| ignoreMessageHeader | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | If true, the message type specified will be ignored and it will automatically attempt to convert the underlying message to the given class | +| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | + +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| Q | The type of message to listen for | +| R | The type of message to respond with | + + +## IContractMetric `type` + +##### Namespace + +MQContract.Interfaces + +##### Summary + +Houses a set of metrics that were requested from the internal metric tracker. +All message conversion durations are calculated from the perspective: + - When a class is being sent from the point of starting the middleware to the point where the class has been encoded into a service message and the middleware has completed + - When a service message is being recieved from the point of starting the middleware to the point where the class has been built from the service message and the middleware has completed + + +### MessageBytes `property` + +##### Summary + +Total amount of bytes from the messages + + +### MessageBytesAverage `property` + +##### Summary + +Average number of bytes from the messages + + +### MessageBytesMax `property` + +##### Summary + +Maximum number of bytes from the messages + + +### MessageBytesMin `property` + +##### Summary + +Minimum number of bytes from the messages + + +### MessageConversionAverage `property` + +##### Summary + +Average time to encode/decode the messages + + +### MessageConversionDuration `property` + +##### Summary + +Total time spent converting the messages + + +### MessageConversionMax `property` + +##### Summary + +Maximum time to encode/decode a message + + +### MessageConversionMin `property` + +##### Summary + +Minimum time to encode/decode a message + + +### Messages `property` + +##### Summary + +Total number of messages + ## IEncodedMessage `type` @@ -310,6 +940,55 @@ The header for the given message The message type id to transmit across + +## IInboxQueryableMessageServiceConnection `type` + +##### Namespace + +MQContract.Interfaces.Service + +##### Summary + +Used to implement an Inbox style query response underlying service, this is if the service does not support QueryResponse messaging but does support a sort of query inbox response +style pub sub where you can specify the destination down to a specific instance. + + +### EstablishInboxSubscriptionAsync(messageReceived,cancellationToken) `method` + +##### Summary + +Establish the inbox subscription with the underlying service connection + +##### Returns + +A service subscription object specifically tied to the RPC inbox for this particular connection instance + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| messageReceived | [System.Action{MQContract.Messages.ReceivedInboxServiceMessage}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Messages.ReceivedInboxServiceMessage}') | Callback called when a message is recieved in the RPC inbox | +| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | + + +### QueryAsync(message,correlationID,cancellationToken) `method` + +##### Summary + +Called to publish a Query Request when using the inbox style + +##### Returns + +The transmission result of submitting the message + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message to submit | +| correlationID | [System.Guid](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Guid 'System.Guid') | The unique ID of the message to use for handling when the response is proper and is expected in the inbox subscription | +| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | + ## IMessageConverter\`2 `type` @@ -320,7 +999,7 @@ MQContract.Interfaces.Conversion ##### Summary Used to define a message converter. These are called upon if a -message is recieved on a channel of type T but it is waiting for +message is received on a channel of type T but it is waiting for message of type V ##### Generic Types @@ -330,8 +1009,8 @@ message of type V | T | The source message type | | V | The destination message type | - -### Convert(source) `method` + +### ConvertAsync(source) `method` ##### Summary @@ -360,8 +1039,8 @@ An implementation of this is used to encode/decode message bodies when specified for a connection. This is to allow for an override of the default encoding of Json for the messages. - -### Decode\`\`1(stream) `method` + +### DecodeAsync\`\`1(stream) `method` ##### Summary @@ -383,8 +1062,8 @@ Null when fails or the value of T that was encoded inside the stream | ---- | ----------- | | T | The type of message being decoded | - -### Encode\`\`1(message) `method` + +### EncodeAsync\`\`1(message) `method` ##### Summary @@ -419,12 +1098,12 @@ An implementation of this is used to encrypt/decrypt message bodies when specified for a connection. This is to allow for extended message security if desired. - -### Decrypt(stream,headers) `method` + +### DecryptAsync(stream,headers) `method` ##### Summary -Called to decrypt the message body stream recieved as a message +Called to decrypt the message body stream received as a message ##### Returns @@ -437,8 +1116,8 @@ A decrypted stream of the message body | stream | [System.IO.Stream](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.IO.Stream 'System.IO.Stream') | The stream representing the message body binary data | | headers | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The message headers that were provided by the message | - -### Encrypt(data,headers) `method` + +### EncryptAsync(data,headers) `method` ##### Summary @@ -458,111 +1137,61 @@ An encrypted byte array of the message body ## IMessageServiceConnection `type` -##### Namespace - -MQContract.Interfaces.Service - -##### Summary - -Defines an underlying service connection. This interface is used to allow for the creation of multiple underlying connection types to support the ability to use common code while -being able to run against 1 or more Message services. - - -### DefaultTimout `property` - -##### Summary - -The default timeout to use for RPC calls when it's not specified - - -### MaxMessageBodySize `property` - -##### Summary - -Maximum supported message body size in bytes - - -### PingAsync() `method` - -##### Summary - -Implemented Ping call if avaialble for the underlying service - -##### Returns - -A Ping Result - -##### Parameters - -This method has no parameters. +##### Namespace - -### PublishAsync(message,options,cancellationToken) `method` +MQContract.Interfaces.Service ##### Summary -Implements a publish call to publish the given message - -##### Returns +Defines an underlying service connection. This interface is used to allow for the creation of multiple underlying connection types to support the ability to use common code while +being able to run against 1 or more Message services. -A transmission result instance indicating the result + +### MaxMessageBodySize `property` -##### Parameters +##### Summary -| Name | Type | Description | -| ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The message to publish | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The Service Channel Options instance that was supplied at the Contract Connection level | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | +Maximum supported message body size in bytes - -### QueryAsync(message,timeout,options,cancellationToken) `method` + +### CloseAsync() `method` ##### Summary -Implements a call to submit a response query request into the underlying service +Implements a call to close off the connection when the ContractConnection is closed ##### Returns -A Query Result instance based on what happened +A task that the close is running in ##### Parameters -| Name | Type | Description | -| ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The message to query with | -| timeout | [System.TimeSpan](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.TimeSpan 'System.TimeSpan') | The timeout for recieving a response | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The Service Channel Options instance that was supplied at the Contract Connection level | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | +This method has no parameters. - -### SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` + +### PublishAsync(message,cancellationToken) `method` ##### Summary -Implements a call to create a subscription to a given channel as a member of a given group +Implements a publish call to publish the given message ##### Returns -A service subscription object +A transmission result instance indicating the result ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| messageRecieved | [System.Action{MQContract.Messages.RecievedServiceMessage}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Messages.RecievedServiceMessage}') | The callback to invoke when a message is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an exception occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to subscribe to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The subscription groupt to subscribe as | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The Service Channel Options instance that was supplied at the Contract Connection level | +| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The message to publish | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -### SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` + +### SubscribeAsync(messageReceived,errorReceived,channel,group,cancellationToken) `method` ##### Summary -Implements a call to create a subscription to a given channel as a member of a given group for responding to queries +Implements a call to create a subscription to a given channel as a member of a given group ##### Returns @@ -572,11 +1201,10 @@ A service subscription object | Name | Type | Description | | ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}') | The callback to be invoked when a message is recieved, returning the response message | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an exception occurs | +| messageReceived | [System.Action{MQContract.Messages.ReceivedServiceMessage}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Messages.ReceivedServiceMessage}') | The callback to invoke when a message is received | +| errorReceived | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an exception occurs | | channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to subscribe to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The subscription groupt to subscribe as | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The Service Channel Options instance that was supplied at the Contract Connection level | +| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The consumer group to register as | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | @@ -597,8 +1225,8 @@ This is used to override the default Json and the Global one for the connection | ---- | ----------- | | T | The type of message that this encoder supports | - -### Decode(stream) `method` + +### DecodeAsync(stream) `method` ##### Summary @@ -614,8 +1242,8 @@ null if the Decode fails, otherwise an instance of the message decoded from the | ---- | ---- | ----------- | | stream | [System.IO.Stream](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.IO.Stream 'System.IO.Stream') | The byte stream containing the encoded message | - -### Encode(message) `method` + +### EncodeAsync(message) `method` ##### Summary @@ -650,71 +1278,75 @@ as well as the default of not encrypting the message body | ---- | ----------- | | T | The type of message that this encryptor supports | - -## IRecievedMessage\`1 `type` + +## IMiddleware `type` ##### Namespace -MQContract.Interfaces +MQContract.Interfaces.Middleware ##### Summary -An interface for describing a Message recieved on a Subscription to be passed into the appropriate callback +Base Middleware just used to limit Generic Types for Register Middleware -##### Generic Types + +## IPingableMessageServiceConnection `type` -| Name | Description | -| ---- | ----------- | -| T | The class type of the underlying message | +##### Namespace - -### Headers `property` +MQContract.Interfaces.Service ##### Summary -The headers that were supplied with the message +Extends the base MessageServiceConnection Interface to support service pinging - -### ID `property` + +### PingAsync() `method` ##### Summary -The unique ID of the recieved message that was specified on the transmission side +Implemented Ping call if avaialble for the underlying service - -### Message `property` +##### Returns -##### Summary +A Ping Result -The message that was transmitted +##### Parameters - -### ProcessedTimestamp `property` +This method has no parameters. -##### Summary + +## IQueryResponseMessageServiceConnection `type` -The timestamp of when the recieved message was converted into the actual class prior to calling the callback +##### Namespace - -### RecievedTimestamp `property` +MQContract.Interfaces.Service ##### Summary -The timestamp of when the message was recieved by the underlying service connection +Extends the base MessageServiceConnection Interface to Response Query messaging methodology if the underlying service supports it - -## IServiceChannelOptions `type` + +### QueryAsync(message,timeout,cancellationToken) `method` -##### Namespace +##### Summary -MQContract.Interfaces.Service +Implements a call to submit a response query request into the underlying service -##### Summary +##### Returns -Used to pass service channel options to the underlying service connection. There are no implemented values this is simply mean to be a class marker. +A Query Result instance based on what happened - -## IServiceSubscription `type` +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The message to query with | +| timeout | [System.TimeSpan](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.TimeSpan 'System.TimeSpan') | The timeout for recieving a response | +| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | + + +## IQueryableMessageServiceConnection `type` ##### Namespace @@ -722,25 +1354,38 @@ MQContract.Interfaces.Service ##### Summary -Represents an underlying service level subscription +Used to identify a message service that supports response query style messaging, either through inbox or directly - -### EndAsync() `method` + +### DefaultTimeout `property` ##### Summary -Called to end the subscription +The default timeout to use for RPC calls when it's not specified + + +### SubscribeQueryAsync(messageReceived,errorReceived,channel,group,cancellationToken) `method` + +##### Summary + +Implements a call to create a subscription to a given channel as a member of a given group for responding to queries ##### Returns -A task to allow for asynchronous ending of the subscription +A service subscription object ##### Parameters -This method has no parameters. +| Name | Type | Description | +| ---- | ---- | ----------- | +| messageReceived | [System.Func{MQContract.Messages.ReceivedServiceMessage,System.Threading.Tasks.ValueTask{MQContract.Messages.ServiceMessage}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Messages.ReceivedServiceMessage,System.Threading.Tasks.ValueTask{MQContract.Messages.ServiceMessage}}') | The callback to be invoked when a message is received, returning the response message | +| errorReceived | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to invoke when an exception occurs | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to subscribe to | +| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The group to bind a consumer to | +| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -## ISubscription `type` + +## IReceivedMessage\`1 `type` ##### Namespace @@ -748,78 +1393,111 @@ MQContract.Interfaces ##### Summary -This interface represents a Contract Connection Subscription and is used to house and end the subscription +An interface for describing a Message received on a Subscription to be passed into the appropriate callback - -### EndAsync() `method` +##### Generic Types + +| Name | Description | +| ---- | ----------- | +| T | The class type of the underlying message | + + +### Headers `property` ##### Summary -Called to end (close off) the subscription +The headers that were supplied with the message -##### Returns + +### ID `property` -A task that is ending the subscription and closing off the resources for it +##### Summary -##### Parameters +The unique ID of the received message that was specified on the transmission side -This method has no parameters. + +### Message `property` + +##### Summary + +The message that was transmitted + + +### ProcessedTimestamp `property` + +##### Summary + +The timestamp of when the received message was converted into the actual class prior to calling the callback - -## InvalidChannelOptionsTypeException `type` + +### ReceivedTimestamp `property` + +##### Summary + +The timestamp of when the message was received by the underlying service connection + + +## IServiceSubscription `type` ##### Namespace -MQContract +MQContract.Interfaces.Service ##### Summary -An exception thrown when the options supplied to an underlying system connection are not of an expected type. +Represents an underlying service level subscription - -### ThrowIfNotNullAndNotOfType(options,expectedTypes) `method` + +### EndAsync() `method` ##### Summary -Called to check if the options is one of the given types +Called to end the subscription + +##### Returns + +A task to allow for asynchronous ending of the subscription ##### Parameters -| Name | Type | Description | -| ---- | ---- | ----------- | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The supplied service channel options | -| expectedTypes | [System.Collections.Generic.IEnumerable{System.Type}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Collections.Generic.IEnumerable 'System.Collections.Generic.IEnumerable{System.Type}') | The possible types it can be | +This method has no parameters. -##### Exceptions + +## ISpecificTypeMiddleware\`1 `type` -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when the options value is not null and not of any of the expected Types | +##### Namespace - -### ThrowIfNotNullAndNotOfType\`\`1(options) `method` +MQContract.Interfaces.Middleware ##### Summary -Called to check if the options is of a given type +Base Specific Type Middleware just used to limit Generic Types for Register Middleware -##### Parameters + +## ISubscription `type` -| Name | Type | Description | -| ---- | ---- | ----------- | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The supplied service channel options | +##### Namespace -##### Generic Types +MQContract.Interfaces -| Name | Description | -| ---- | ----------- | -| T | The expected type for the ServiceChannelOptions | +##### Summary -##### Exceptions +This interface represents a Contract Connection Subscription and is used to house and end the subscription -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when the options value is not null and not of type T | + +### EndAsync() `method` + +##### Summary + +Called to end (close off) the subscription + +##### Returns + +A task that is ending the subscription and closing off the resources for it + +##### Parameters + +This method has no parameters. ## MessageChannelAttribute `type` @@ -1044,6 +1722,13 @@ be overridden by supplying a timeout value when making an RPC call. + +### TimeSpanValue `property` + +##### Summary + +The converted TimeSpan value from the supplied milliseconds value in the constructor + ### Value `property` @@ -1104,36 +1789,6 @@ it allows you to not necessarily update code for call handling immediately. The version number to tag this class with during transmission - -## NoChannelOptionsAvailableException `type` - -##### Namespace - -MQContract - -##### Summary - -An exception thrown when there are options supplied to an underlying system connection that does not support options for that particular instance - - -### ThrowIfNotNull(options) `method` - -##### Summary - -Called to throw if options is not null - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The service channel options that were supplied | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown when the options is not null | - ## PingResult `type` @@ -1187,6 +1842,45 @@ How long it took for the server to respond The version of the service running, if provided + +## QueryResponseChannelAttribute `type` + +##### Namespace + +MQContract.Attributes + +##### Summary + +Used to allow the specification of a response channel to be used without supplying it to the contract calls. +IMPORTANT: This particular attribute and the response channel argument are only used when the underlying connection does not support QueryResponse messaging. + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| name | [T:MQContract.Attributes.QueryResponseChannelAttribute](#T-T-MQContract-Attributes-QueryResponseChannelAttribute 'T:MQContract.Attributes.QueryResponseChannelAttribute') | The name of the channel to use for responses | + + +### #ctor(name) `constructor` + +##### Summary + +Used to allow the specification of a response channel to be used without supplying it to the contract calls. +IMPORTANT: This particular attribute and the response channel argument are only used when the underlying connection does not support QueryResponse messaging. + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| name | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to use for responses | + + +### Name `property` + +##### Summary + +The Name of the response channel + ## QueryResponseMessage\`1 `type` @@ -1348,8 +2042,8 @@ The response headers The resulting response if there was one - -## RecievedServiceMessage `type` + +## ReceivedInboxServiceMessage `type` ##### Namespace @@ -1357,20 +2051,63 @@ MQContract.Messages ##### Summary -A Recieved Service Message that gets passed back up into the Contract Connection when a message is recieved from the underlying service connection +A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| ID | [T:MQContract.Messages.RecievedServiceMessage](#T-T-MQContract-Messages-RecievedServiceMessage 'T:MQContract.Messages.RecievedServiceMessage') | The unique ID of the message | +| ID | [T:MQContract.Messages.ReceivedInboxServiceMessage](#T-T-MQContract-Messages-ReceivedInboxServiceMessage 'T:MQContract.Messages.ReceivedInboxServiceMessage') | The unique ID of the message | - -### #ctor(ID,MessageTypeID,Channel,Header,Data) `constructor` + +### #ctor(ID,MessageTypeID,Channel,Header,CorrelationID,Data,Acknowledge) `constructor` + +##### Summary + +A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| ID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The unique ID of the message | +| MessageTypeID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The message type id which is used for decoding to a class | +| Channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel the message was received on | +| Header | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The message headers that came through | +| CorrelationID | [System.Guid](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Guid 'System.Guid') | The query message correlation id supplied by the query call to tie to the response | +| Data | [System.ReadOnlyMemory{System.Byte}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.ReadOnlyMemory 'System.ReadOnlyMemory{System.Byte}') | The binary content of the message that should be the encoded class | +| Acknowledge | [System.Func{System.Threading.Tasks.ValueTask}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.Threading.Tasks.ValueTask}') | The acknowledgement callback to be called when the message is received if the underlying service requires it | + + +### CorrelationID `property` + +##### Summary + +The query message correlation id supplied by the query call to tie to the response + + +## ReceivedServiceMessage `type` + +##### Namespace + +MQContract.Messages + +##### Summary + +A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| ID | [T:MQContract.Messages.ReceivedServiceMessage](#T-T-MQContract-Messages-ReceivedServiceMessage 'T:MQContract.Messages.ReceivedServiceMessage') | The unique ID of the message | + + +### #ctor(ID,MessageTypeID,Channel,Header,Data,Acknowledge) `constructor` ##### Summary -A Recieved Service Message that gets passed back up into the Contract Connection when a message is recieved from the underlying service connection +A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection ##### Parameters @@ -1378,16 +2115,24 @@ A Recieved Service Message that gets passed back up into the Contract Connection | ---- | ---- | ----------- | | ID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The unique ID of the message | | MessageTypeID | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The message type id which is used for decoding to a class | -| Channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel the message was recieved on | +| Channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel the message was received on | | Header | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | The message headers that came through | | Data | [System.ReadOnlyMemory{System.Byte}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.ReadOnlyMemory 'System.ReadOnlyMemory{System.Byte}') | The binary content of the message that should be the encoded class | +| Acknowledge | [System.Func{System.Threading.Tasks.ValueTask}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.Threading.Tasks.ValueTask}') | The acknowledgement callback to be called when the message is received if the underlying service requires it | + + +### Acknowledge `property` + +##### Summary + +The acknowledgement callback to be called when the message is received if the underlying service requires it - -### RecievedTimestamp `property` + +### ReceivedTimestamp `property` ##### Summary -A timestamp for when the message was recieved +A timestamp for when the message was received ## ServiceMessage `type` diff --git a/AutomatedTesting/AutomatedTesting.csproj b/AutomatedTesting/AutomatedTesting.csproj index 9a85329..9ed3c83 100644 --- a/AutomatedTesting/AutomatedTesting.csproj +++ b/AutomatedTesting/AutomatedTesting.csproj @@ -10,10 +10,11 @@ - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/AutomatedTesting/ChannelMapperTests.cs b/AutomatedTesting/ChannelMapperTests.cs index 3e405b4..b44d217 100644 --- a/AutomatedTesting/ChannelMapperTests.cs +++ b/AutomatedTesting/ChannelMapperTests.cs @@ -19,21 +19,18 @@ public async Task TestPublishMapWithStringToString() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; var mapper = new ChannelMapper() .AddPublishMap(typeof(BasicMessage).GetCustomAttribute(false)?.Name!, newChannel); - var contractConnection = new ContractConnection(serviceConnection.Object,channelMapper:mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object,channelMapper:mapper); #endregion #region Act @@ -51,7 +48,7 @@ public async Task TestPublishMapWithStringToString() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -62,15 +59,12 @@ public async Task TestPublishMapWithStringToFunction() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; var otherChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-OtherChannel"; @@ -78,11 +72,11 @@ public async Task TestPublishMapWithStringToFunction() .AddPublishMap(typeof(BasicMessage).GetCustomAttribute(false)?.Name!, (originalChannel) => { if (Equals(originalChannel, typeof(BasicMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -104,7 +98,7 @@ public async Task TestPublishMapWithStringToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -115,16 +109,13 @@ public async Task TestPublishMapWithMatchToFunction() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); - + List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - + var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; var otherChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-OtherChannel"; var mapper = new ChannelMapper() @@ -132,10 +123,10 @@ public async Task TestPublishMapWithMatchToFunction() (channelName)=>Equals(channelName, otherChannel) ,(originalChannel) => { - return Task.FromResult(newChannel); + return ValueTask.FromResult(newChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -157,7 +148,7 @@ public async Task TestPublishMapWithMatchToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -168,27 +159,24 @@ public async Task TestPublishMapWithDefaultFunction() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); - + List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - + var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; var otherChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-OtherChannel"; var mapper = new ChannelMapper() .AddDefaultPublishMap((originalChannel) => { if (Equals(originalChannel, typeof(BasicMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -210,7 +198,7 @@ public async Task TestPublishMapWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -221,26 +209,23 @@ public async Task TestPublishMapWithSingleMapAndWithDefaultFunction() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); - + List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - + var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; var otherChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-OtherChannel"; var mapper = new ChannelMapper() .AddPublishMap(typeof(BasicMessage).GetCustomAttribute(false)?.Name!,newChannel) .AddDefaultPublishMap((originalChannel) => { - return Task.FromResult(originalChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -262,7 +247,7 @@ public async Task TestPublishMapWithSingleMapAndWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -275,22 +260,23 @@ public async Task TestPublishSubscribeMapWithStringToString() var channels = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), - It.IsAny>(), + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), + It.IsAny>(), Capture.In(channels), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(serviceSubscription.Object); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; var mapper = new ChannelMapper() .AddPublishSubscriptionMap(typeof(BasicMessage).GetCustomAttribute(false)?.Name!, newChannel); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act var subscription = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }); #endregion @@ -301,8 +287,7 @@ public async Task TestPublishSubscribeMapWithStringToString() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -315,10 +300,11 @@ public async Task TestPublishSubscribeMapWithStringToFunction() var channels = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), Capture.In(channels), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(serviceSubscription.Object); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -329,21 +315,21 @@ public async Task TestPublishSubscribeMapWithStringToFunction() (originalChannel) => { if (Equals(originalChannel, typeof(BasicMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act var subscription1 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }); var subscription2 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }, - channel:otherChannel); + channel: otherChannel); #endregion #region Assert @@ -355,8 +341,7 @@ public async Task TestPublishSubscribeMapWithStringToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -369,10 +354,11 @@ public async Task TestPublishSubscribeMapWithMatchToFunction() var channels = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), Capture.In(channels), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(serviceSubscription.Object); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -382,18 +368,18 @@ public async Task TestPublishSubscribeMapWithMatchToFunction() (channelName) => Equals(channelName, otherChannel), (originalChannel) => { - return Task.FromResult(newChannel); + return ValueTask.FromResult(newChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act var subscription1 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }); var subscription2 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }, channel: otherChannel); #endregion @@ -407,8 +393,7 @@ public async Task TestPublishSubscribeMapWithMatchToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -421,10 +406,11 @@ public async Task TestPublishSubscribeMapWithDefaultFunction() var channels = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), Capture.In(channels), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(serviceSubscription.Object); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -434,19 +420,19 @@ public async Task TestPublishSubscribeMapWithDefaultFunction() (originalChannel) => { if (Equals(originalChannel, typeof(BasicMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act var subscription1 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }); var subscription2 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }, channel: otherChannel); #endregion @@ -460,8 +446,7 @@ public async Task TestPublishSubscribeMapWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -474,10 +459,11 @@ public async Task TestPublishSubscribeMapWithSingleMapAndWithDefaultFunction() var channels = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), Capture.In(channels), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(serviceSubscription.Object); var newChannel = $"{typeof(BasicMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -487,18 +473,18 @@ public async Task TestPublishSubscribeMapWithSingleMapAndWithDefaultFunction() .AddDefaultPublishSubscriptionMap( (originalChannel) => { - return Task.FromResult(originalChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act var subscription1 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }); var subscription2 = await contractConnection.SubscribeAsync( - (msg) => Task.CompletedTask, + (msg) => ValueTask.CompletedTask, (error) => { }, channel: otherChannel); #endregion @@ -512,8 +498,7 @@ public async Task TestPublishSubscribeMapWithSingleMapAndWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -534,17 +519,17 @@ public async Task TestQueryMapWithStringToString() List messages = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var newChannel = $"{typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name}-Modded"; var mapper = new ChannelMapper() .AddQueryMap(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name!, newChannel); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -564,7 +549,7 @@ public async Task TestQueryMapWithStringToString() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(),It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -585,10 +570,10 @@ public async Task TestQueryMapWithStringToFunction() List messages = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var newChannel = $"{typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -598,11 +583,11 @@ public async Task TestQueryMapWithStringToFunction() (originalChannel) => { if (Equals(originalChannel, typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -622,7 +607,7 @@ public async Task TestQueryMapWithStringToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -643,10 +628,10 @@ public async Task TestQueryMapWithMatchToFunction() List messages = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var newChannel = $"{typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -655,10 +640,10 @@ public async Task TestQueryMapWithMatchToFunction() .AddQueryMap((channelName) => Equals(channelName, otherChannel) , (originalChannel) => { - return Task.FromResult(newChannel); + return ValueTask.FromResult(newChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -678,7 +663,7 @@ public async Task TestQueryMapWithMatchToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -699,10 +684,10 @@ public async Task TestQueryMapWithDefaultFunction() List messages = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var newChannel = $"{typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -711,11 +696,11 @@ public async Task TestQueryMapWithDefaultFunction() .AddDefaultQueryMap((originalChannel) => { if (Equals(originalChannel, typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -735,7 +720,7 @@ public async Task TestQueryMapWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -756,10 +741,10 @@ public async Task TestQueryMapWithSingleMapAndWithDefaultFunction() List messages = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), It.IsAny(), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var newChannel = $"{typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name}-Modded"; @@ -768,10 +753,10 @@ public async Task TestQueryMapWithSingleMapAndWithDefaultFunction() .AddQueryMap(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name!, newChannel) .AddDefaultQueryMap((originalChannel) => { - return Task.FromResult(originalChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act @@ -791,7 +776,7 @@ public async Task TestQueryMapWithSingleMapAndWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -803,13 +788,12 @@ public async Task TestQuerySubscribeMapWithStringToString() var channels = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), Capture.In(channels), It.IsAny(), - It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); @@ -817,13 +801,14 @@ public async Task TestQuerySubscribeMapWithStringToString() var mapper = new ChannelMapper() .AddQuerySubscriptionMap(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name!, newChannel); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act - var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); - }, (error) => {}); + }, (error) => { }); #endregion #region Assert @@ -834,8 +819,7 @@ public async Task TestQuerySubscribeMapWithStringToString() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -847,13 +831,12 @@ public async Task TestQuerySubscribeMapWithStringToFunction() var channels = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), Capture.In(channels), It.IsAny(), - It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); @@ -865,21 +848,23 @@ public async Task TestQuerySubscribeMapWithStringToFunction() (originalChannel) => { if (Equals(originalChannel, typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act - var subscription1 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription1 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }); - var subscription2 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription2 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }, - channel:otherChannel); + channel: otherChannel); #endregion #region Assert @@ -892,8 +877,7 @@ public async Task TestQuerySubscribeMapWithStringToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -905,13 +889,12 @@ public async Task TestQuerySubscribeMapWithMatchToFunction() var channels = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), Capture.In(channels), It.IsAny(), - It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); @@ -922,17 +905,19 @@ public async Task TestQuerySubscribeMapWithMatchToFunction() (channelName) => Equals(channelName, otherChannel), (originalChannel) => { - return Task.FromResult(newChannel); + return ValueTask.FromResult(newChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act - var subscription1 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription1 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }); - var subscription2 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription2 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }, channel: otherChannel); @@ -948,8 +933,7 @@ public async Task TestQuerySubscribeMapWithMatchToFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -961,13 +945,12 @@ public async Task TestQuerySubscribeMapWithDefaultFunction() var channels = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), Capture.In(channels), It.IsAny(), - It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); @@ -978,18 +961,20 @@ public async Task TestQuerySubscribeMapWithDefaultFunction() (originalChannel) => { if (Equals(originalChannel, typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name)) - return Task.FromResult(newChannel); - return Task.FromResult(originalChannel); + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act - var subscription1 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription1 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }); - var subscription2 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription2 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }, channel: otherChannel); @@ -1005,8 +990,7 @@ public async Task TestQuerySubscribeMapWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -1018,13 +1002,12 @@ public async Task TestQuerySubscribeMapWithSingleMapAndWithDefaultFunction() var channels = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), Capture.In(channels), It.IsAny(), - It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); @@ -1035,17 +1018,19 @@ public async Task TestQuerySubscribeMapWithSingleMapAndWithDefaultFunction() .AddDefaultQuerySubscriptionMap( (originalChannel) => { - return Task.FromResult(originalChannel); + return ValueTask.FromResult(originalChannel); }); - var contractConnection = new ContractConnection(serviceConnection.Object, channelMapper: mapper); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); #endregion #region Act - var subscription1 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription1 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }); - var subscription2 = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription2 = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { throw new NotImplementedException(); }, (error) => { }, channel: otherChannel); @@ -1061,8 +1046,359 @@ public async Task TestQuerySubscribeMapWithSingleMapAndWithDefaultFunction() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Exactly(2)); + #endregion + } + + private const string REPLY_CHANNEL_HEADER = "_QueryReplyChannel"; + + [TestMethod] + public async Task TestQueryResponseMapWithStringToString() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); + var mockSubscription = new Mock(); + + + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + if (message.Header[REPLY_CHANNEL_HEADER]!=null) + channels.Add(message.Header[REPLY_CHANNEL_HEADER]!); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", message.Channel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var responseChannel = "Gretting.Response"; + + var newChannel = $"{responseChannel}-Modded"; + var mapper = new ChannelMapper() + .AddQueryResponseMap(responseChannel, newChannel); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(channels, 1, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result); + Assert.IsNull(result.Error); + Assert.IsFalse(result.IsError); + Assert.AreEqual(newChannel, channels[0]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryResponseMapWithStringToFunction() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); + var mockSubscription = new Mock(); + + + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + if (message.Header[REPLY_CHANNEL_HEADER]!=null) + channels.Add(message.Header[REPLY_CHANNEL_HEADER]!); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", message.Channel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var responseChannel = "Gretting.Response"; + + var newChannel = $"{responseChannel}-Modded"; + var otherChannel = $"{responseChannel}-OtherChannel"; + var mapper = new ChannelMapper() + .AddQueryResponseMap(responseChannel, + (originalChannel) => + { + if (Equals(originalChannel, responseChannel)) + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result1 = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + var result2 = await contractConnection.QueryAsync(testMessage, responseChannel: otherChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(channels, 2, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result1); + Assert.IsNotNull(result2); + Assert.AreEqual(newChannel, channels[0]); + Assert.AreEqual(otherChannel, channels[1]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + mockSubscription.Verify(x => x.EndAsync(), Times.Exactly(2)); + #endregion + } + + [TestMethod] + public async Task TestQueryResponseMapWithMatchToFunction() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); + var mockSubscription = new Mock(); + + + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + if (message.Header[REPLY_CHANNEL_HEADER]!=null) + channels.Add(message.Header[REPLY_CHANNEL_HEADER]!); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", message.Channel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var responseChannel = "Gretting.Response"; + + var newChannel = $"{responseChannel}-Modded"; + var otherChannel = $"{responseChannel}-OtherChannel"; + var mapper = new ChannelMapper() + .AddQueryResponseMap((channelName) => Equals(channelName, otherChannel) + , (originalChannel) => + { + return ValueTask.FromResult(newChannel); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result1 = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + var result2 = await contractConnection.QueryAsync(testMessage, responseChannel: otherChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(channels, 2, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result1); + Assert.IsNotNull(result2); + Assert.AreEqual(responseChannel, channels[0]); + Assert.AreEqual(newChannel, channels[1]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + mockSubscription.Verify(x => x.EndAsync(), Times.Exactly(2)); + #endregion + } + + [TestMethod] + public async Task TestQueryResponseMapWithDefaultFunction() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); + var mockSubscription = new Mock(); + + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + if (message.Header[REPLY_CHANNEL_HEADER]!=null) + channels.Add(message.Header[REPLY_CHANNEL_HEADER]!); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", message.Channel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var responseChannel = "Gretting.Response"; + + var newChannel = $"{responseChannel}-Modded"; + var otherChannel = $"{responseChannel}-OtherChannel"; + var mapper = new ChannelMapper() + .AddDefaultQueryResponseMap((originalChannel) => + { + if (Equals(originalChannel, responseChannel)) + return ValueTask.FromResult(newChannel); + return ValueTask.FromResult(originalChannel); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result1 = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + var result2 = await contractConnection.QueryAsync(testMessage, responseChannel: otherChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(channels, 2, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result1); + Assert.IsNotNull(result2); + Assert.AreEqual(newChannel, channels[0]); + Assert.AreEqual(otherChannel, channels[1]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + mockSubscription.Verify(x => x.EndAsync(), Times.Exactly(2)); + #endregion + } + + [TestMethod] + public async Task TestQueryResponseMapWithSingleMapAndWithDefaultFunction() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); + var mockSubscription = new Mock(); + + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + if (message.Header[REPLY_CHANNEL_HEADER]!=null) + channels.Add(message.Header[REPLY_CHANNEL_HEADER]!); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", message.Channel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var responseChannel = "Gretting.Response"; + + var newChannel = $"{responseChannel}-Modded"; + var otherChannel = $"{responseChannel}-OtherChannel"; + var mapper = new ChannelMapper() + .AddQueryResponseMap(responseChannel, newChannel) + .AddDefaultQueryResponseMap((originalChannel) => + { + return ValueTask.FromResult(originalChannel); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object, channelMapper: mapper); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result1 = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + var result2 = await contractConnection.QueryAsync(testMessage, responseChannel: otherChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(channels, 2, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result1); + Assert.IsNotNull(result2); + Assert.AreEqual(newChannel, channels[0]); + Assert.AreEqual(otherChannel, channels[1]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + mockSubscription.Verify(x => x.EndAsync(), Times.Exactly(2)); #endregion } } diff --git a/AutomatedTesting/ContractConnectionTests/CleanupTests.cs b/AutomatedTesting/ContractConnectionTests/CleanupTests.cs new file mode 100644 index 0000000..b276f5e --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/CleanupTests.cs @@ -0,0 +1,140 @@ +using Moq; +using MQContract.Interfaces.Service; +using MQContract; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class CleanupTests + { + [TestMethod] + public async Task TestCloseAsync() + { + #region Arrange + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.CloseAsync()) + .Returns(ValueTask.CompletedTask); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + await contractConnection.CloseAsync(); + #endregion + + #region Assert + #endregion + + #region Verify + serviceConnection.Verify(x => x.CloseAsync(), Times.Once); + #endregion + } + + [TestMethod] + public void TestDispose() + { + #region Arrange + var serviceConnection = new Mock(); + + var contractConnection = ContractConnection.Instance(serviceConnection.As().Object); + #endregion + + #region Act + contractConnection.Dispose(); + #endregion + + #region Assert + #endregion + + #region Verify + serviceConnection.Verify(x => x.Dispose(), Times.Once); + #endregion + } + + [TestMethod] + public void TestDisposeWithAsyncDispose() + { + #region Arrange + var serviceConnection = new Mock(); + serviceConnection.Setup(x=>x.DisposeAsync()).Returns(ValueTask.CompletedTask); + + var contractConnection = ContractConnection.Instance(serviceConnection.As().Object); + #endregion + + #region Act + contractConnection.Dispose(); + #endregion + + #region Assert + #endregion + + #region Verify + serviceConnection.Verify(x => x.DisposeAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestDisposeAsyncWithDispose() + { + #region Arrange + var serviceConnection = new Mock(); + + var contractConnection = ContractConnection.Instance(serviceConnection.As().Object); + #endregion + + #region Act + await contractConnection.DisposeAsync(); + #endregion + + #region Assert + #endregion + + #region Verify + serviceConnection.Verify(x => x.Dispose(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestDisposeAsyncWithDisposeAsync() + { + #region Arrange + var serviceConnection = new Mock(); + + var contractConnection = ContractConnection.Instance(serviceConnection.As().Object); + #endregion + + #region Act + await contractConnection.DisposeAsync(); + #endregion + + #region Assert + #endregion + + #region Verify + serviceConnection.Verify(x => x.DisposeAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestDisposeAsyncWithDisposeAsyncAndDispose() + { + #region Arrange + var serviceConnection = new Mock(); + + var contractConnection = ContractConnection.Instance(serviceConnection.As().As().Object); + #endregion + + #region Act + await contractConnection.DisposeAsync(); + #endregion + + #region Assert + #endregion + + #region Verify + serviceConnection.Verify(x => x.DisposeAsync(), Times.Once); + serviceConnection.As().Verify(x => x.Dispose(), Times.Never); + #endregion + } + } +} diff --git a/AutomatedTesting/ContractConnectionTests/InternalMetricTests.cs b/AutomatedTesting/ContractConnectionTests/InternalMetricTests.cs new file mode 100644 index 0000000..0c7f40d --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/InternalMetricTests.cs @@ -0,0 +1,325 @@ +using AutomatedTesting.Messages; +using Moq; +using MQContract.Interfaces.Service; +using MQContract; +using MQContract.Interfaces; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class InternalMetricTests + { + [TestMethod] + public void TestInternalMetricInitialization() + { + #region Arrange + var channel = "TestInternalMetricsChannel"; + + var serviceConnection = new Mock(); + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(null, true); + #endregion + + #region Act + IContractMetric?[] sentMetrics = [ + contractConnection.GetSnapshot(true), + contractConnection.GetSnapshot(typeof(BasicMessage),true), + contractConnection.GetSnapshot(true), + contractConnection.GetSnapshot(channel,true) + ]; + IContractMetric?[] receivedMetrics = [ + contractConnection.GetSnapshot(false), + contractConnection.GetSnapshot(typeof(BasicMessage),false), + contractConnection.GetSnapshot(false), + contractConnection.GetSnapshot(channel,false) + ]; + #endregion + + #region Assert + Assert.IsNotNull(sentMetrics[0]); + Assert.IsNotNull(receivedMetrics[0]); + Assert.IsFalse(sentMetrics.Skip(1).Any(m => m!=null)); + Assert.AreEqual(ulong.MaxValue, sentMetrics[0]?.MessageBytesMin); + Assert.AreEqual(0, sentMetrics[0]?.MessageBytes); + Assert.AreEqual(ulong.MinValue, sentMetrics[0]?.MessageBytesMax); + Assert.AreEqual(0, sentMetrics[0]?.MessageBytesAverage); + Assert.AreEqual(0, sentMetrics[0]?.Messages); + Assert.AreEqual(TimeSpan.MaxValue, sentMetrics[0]?.MessageConversionMin); + Assert.AreEqual(TimeSpan.MinValue, sentMetrics[0]?.MessageConversionMax); + Assert.AreEqual(TimeSpan.Zero, sentMetrics[0]?.MessageConversionAverage); + Assert.AreEqual(TimeSpan.Zero, sentMetrics[0]?.MessageConversionDuration); + + Assert.IsFalse(receivedMetrics.Skip(1).Any(m => m!=null)); + Assert.AreEqual(ulong.MaxValue, receivedMetrics[0]?.MessageBytesMin); + Assert.AreEqual(0, receivedMetrics[0]?.MessageBytes); + Assert.AreEqual(ulong.MinValue, receivedMetrics[0]?.MessageBytesMax); + Assert.AreEqual(0, receivedMetrics[0]?.MessageBytesAverage); + Assert.AreEqual(0, receivedMetrics[0]?.Messages); + Assert.AreEqual(TimeSpan.MaxValue, receivedMetrics[0]?.MessageConversionMin); + Assert.AreEqual(TimeSpan.MinValue, receivedMetrics[0]?.MessageConversionMax); + Assert.AreEqual(TimeSpan.Zero, receivedMetrics[0]?.MessageConversionAverage); + Assert.AreEqual(TimeSpan.Zero, receivedMetrics[0]?.MessageConversionDuration); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public void TestInternalMetricGetSnapshotsWithoutEnabling() + { + #region Arrange + var channel = "TestInternalMetricsChannel"; + + var serviceConnection = new Mock(); + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(null, false); + #endregion + + #region Act + IContractMetric?[] sentMetrics = [ + contractConnection.GetSnapshot(true), + contractConnection.GetSnapshot(typeof(BasicMessage),true), + contractConnection.GetSnapshot(true), + contractConnection.GetSnapshot(channel,true) + ]; + IContractMetric?[] receivedMetrics = [ + contractConnection.GetSnapshot(false), + contractConnection.GetSnapshot(typeof(BasicMessage),false), + contractConnection.GetSnapshot(false), + contractConnection.GetSnapshot(channel,false) + ]; + #endregion + + #region Assert + Assert.IsTrue(Array.TrueForAll(sentMetrics,m => m==null)); + Assert.IsTrue(Array.TrueForAll(receivedMetrics,m => m==null)); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public async Task TestPublishAsyncSubscribeInternalMetrics() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + var serviceSubscription = new Mock(); + + var testMessage = new BasicMessage("testMessage"); + var channel = "TestInternalMetricsChannel"; + + List messages = []; + var actions = new List>(); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + var rmessage = Helper.ProduceReceivedServiceMessage(message); + foreach (var act in actions) + act(rmessage); + return ValueTask.FromResult(transmissionResult); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(null,true); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeAsync( + (msg) => ValueTask.CompletedTask, + (error) => { }, + channel:channel); + var result = await contractConnection.PublishAsync(testMessage,channel:channel); + _ = await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1)); + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(true); + IContractMetric?[] sentMetrics = [ + contractConnection.GetSnapshot(true), + contractConnection.GetSnapshot(typeof(BasicMessage),true), + contractConnection.GetSnapshot(true), + contractConnection.GetSnapshot(channel,true) + ]; + IContractMetric?[] receivedMetrics = [ + contractConnection.GetSnapshot(false), + contractConnection.GetSnapshot(typeof(BasicMessage),false), + contractConnection.GetSnapshot(false), + contractConnection.GetSnapshot(channel,false) + ]; + #endregion + + #region Assert + Assert.IsNotNull(result); + Assert.AreEqual(transmissionResult, result); + Assert.IsFalse(Array.Exists(sentMetrics,(m => m==null))); + Assert.AreEqual(1, sentMetrics[0]?.Messages); + Assert.IsTrue(Array.TrueForAll(sentMetrics,(m => + Equals(sentMetrics[0]?.MessageBytesMin,m?.MessageBytesMin) && + Equals(sentMetrics[0]?.MessageBytes, m?.MessageBytes) && + Equals(sentMetrics[0]?.MessageBytesMax, m?.MessageBytesMax) && + Equals(sentMetrics[0]?.MessageBytesAverage, m?.MessageBytesAverage) && + Equals(sentMetrics[0]?.Messages, m?.Messages) && + Equals(sentMetrics[0]?.MessageConversionMin, m?.MessageConversionMin) && + Equals(sentMetrics[0]?.MessageConversionMax, m?.MessageConversionMax) && + Equals(sentMetrics[0]?.MessageConversionAverage, m?.MessageConversionAverage) && + Equals(sentMetrics[0]?.MessageConversionDuration, m?.MessageConversionDuration) + ))); + + Assert.IsFalse(Array.Exists(receivedMetrics, (m => m==null))); + Assert.AreEqual(1, receivedMetrics[0]?.Messages); + Assert.IsTrue(Array.TrueForAll(receivedMetrics,(m => + Equals(receivedMetrics[0]?.MessageBytesMin, m?.MessageBytesMin) && + Equals(receivedMetrics[0]?.MessageBytes, m?.MessageBytes) && + Equals(receivedMetrics[0]?.MessageBytesMax, m?.MessageBytesMax) && + Equals(receivedMetrics[0]?.MessageBytesAverage, m?.MessageBytesAverage) && + Equals(receivedMetrics[0]?.Messages, m?.Messages) && + Equals(receivedMetrics[0]?.MessageConversionMin, m?.MessageConversionMin) && + Equals(receivedMetrics[0]?.MessageConversionMax, m?.MessageConversionMax) && + Equals(receivedMetrics[0]?.MessageConversionAverage, m?.MessageConversionAverage) && + Equals(receivedMetrics[0]?.MessageConversionDuration, m?.MessageConversionDuration) + ))); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncWithNoExtendedAspects() + { + #region Arrange + var serviceSubscription = new Mock(); + + var receivedActions = new List>>(); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeQueryAsync( + Capture.In>>(receivedActions), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) => + { + var rmessage = Helper.ProduceReceivedServiceMessage(message); + var result = await receivedActions[0](rmessage); + return Helper.ProduceQueryResult(result); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(null, true); + + var message = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + var responseMessage = new BasicResponseMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + var channel = "TestQueryMetricChannel"; + #endregion + + #region Act + _ = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { + return ValueTask.FromResult(new QueryResponseMessage(responseMessage, null)); + }, (error) => { }, + channel:channel); + _ = await contractConnection.QueryAsync(message,channel:channel); + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(true); + IContractMetric?[] querySentMetrics = [ + contractConnection.GetSnapshot(typeof(BasicQueryMessage),true), + contractConnection.GetSnapshot(true) + ]; + IContractMetric?[] queryReceivedMetrics = [ + contractConnection.GetSnapshot(typeof(BasicQueryMessage),false), + contractConnection.GetSnapshot(false) + ]; + IContractMetric?[] responseSentMetrics = [ + contractConnection.GetSnapshot(typeof(BasicResponseMessage),true), + contractConnection.GetSnapshot(true) + ]; + IContractMetric?[] responseReceivedMetrics = [ + contractConnection.GetSnapshot(typeof(BasicResponseMessage),false), + contractConnection.GetSnapshot(false) + ]; + IContractMetric? globalSentMetrics = contractConnection.GetSnapshot(true); + IContractMetric? globalReceivedMetrics = contractConnection.GetSnapshot(false); + IContractMetric? channelSentMetrics = contractConnection.GetSnapshot(channel, true); + IContractMetric? channelReceivedMetrics = contractConnection.GetSnapshot(channel, false); + #endregion + + #region Assert + Assert.IsNotNull(globalSentMetrics); + Assert.AreEqual(2, globalSentMetrics.Messages); + Assert.IsNotNull(globalReceivedMetrics); + Assert.AreEqual(2, globalReceivedMetrics.Messages); + Assert.IsNotNull(channelSentMetrics); + Assert.AreEqual(1, channelSentMetrics.Messages); + Assert.IsNotNull(channelReceivedMetrics); + Assert.AreEqual(1, channelReceivedMetrics.Messages); + Assert.IsTrue(Array.TrueForAll(querySentMetrics, (q) => q!=null)); + Assert.IsTrue(Array.TrueForAll(queryReceivedMetrics, (q) => q!=null)); + Assert.IsTrue(Array.TrueForAll(responseSentMetrics, (q) => q!=null)); + Assert.IsTrue(Array.TrueForAll(responseReceivedMetrics, (q) => q!=null)); + Assert.AreEqual(1, querySentMetrics[0]?.Messages); + Assert.IsTrue(Array.TrueForAll(querySentMetrics, (m => + Equals(querySentMetrics[0]?.MessageBytesMin, m?.MessageBytesMin) && + Equals(querySentMetrics[0]?.MessageBytes, m?.MessageBytes) && + Equals(querySentMetrics[0]?.MessageBytesMax, m?.MessageBytesMax) && + Equals(querySentMetrics[0]?.MessageBytesAverage, m?.MessageBytesAverage) && + Equals(querySentMetrics[0]?.Messages, m?.Messages) && + Equals(querySentMetrics[0]?.MessageConversionMin, m?.MessageConversionMin) && + Equals(querySentMetrics[0]?.MessageConversionMax, m?.MessageConversionMax) && + Equals(querySentMetrics[0]?.MessageConversionAverage, m?.MessageConversionAverage) && + Equals(querySentMetrics[0]?.MessageConversionDuration, m?.MessageConversionDuration) + ))); + Assert.AreEqual(1, queryReceivedMetrics[0]?.Messages); + Assert.IsTrue(Array.TrueForAll(queryReceivedMetrics, (m => + Equals(queryReceivedMetrics[0]?.MessageBytesMin, m?.MessageBytesMin) && + Equals(queryReceivedMetrics[0]?.MessageBytes, m?.MessageBytes) && + Equals(queryReceivedMetrics[0]?.MessageBytesMax, m?.MessageBytesMax) && + Equals(queryReceivedMetrics[0]?.MessageBytesAverage, m?.MessageBytesAverage) && + Equals(queryReceivedMetrics[0]?.Messages, m?.Messages) && + Equals(queryReceivedMetrics[0]?.MessageConversionMin, m?.MessageConversionMin) && + Equals(queryReceivedMetrics[0]?.MessageConversionMax, m?.MessageConversionMax) && + Equals(queryReceivedMetrics[0]?.MessageConversionAverage, m?.MessageConversionAverage) && + Equals(queryReceivedMetrics[0]?.MessageConversionDuration, m?.MessageConversionDuration) + ))); + Assert.AreEqual(1, responseSentMetrics[0]?.Messages); + Assert.IsTrue(Array.TrueForAll(responseSentMetrics, (m => + Equals(responseSentMetrics[0]?.MessageBytesMin, m?.MessageBytesMin) && + Equals(responseSentMetrics[0]?.MessageBytes, m?.MessageBytes) && + Equals(responseSentMetrics[0]?.MessageBytesMax, m?.MessageBytesMax) && + Equals(responseSentMetrics[0]?.MessageBytesAverage, m?.MessageBytesAverage) && + Equals(responseSentMetrics[0]?.Messages, m?.Messages) && + Equals(responseSentMetrics[0]?.MessageConversionMin, m?.MessageConversionMin) && + Equals(responseSentMetrics[0]?.MessageConversionMax, m?.MessageConversionMax) && + Equals(responseSentMetrics[0]?.MessageConversionAverage, m?.MessageConversionAverage) && + Equals(responseSentMetrics[0]?.MessageConversionDuration, m?.MessageConversionDuration) + ))); + Assert.AreEqual(1, responseReceivedMetrics[0]?.Messages); + Assert.IsTrue(Array.TrueForAll(responseReceivedMetrics, (m => + Equals(responseReceivedMetrics[0]?.MessageBytesMin, m?.MessageBytesMin) && + Equals(responseReceivedMetrics[0]?.MessageBytes, m?.MessageBytes) && + Equals(responseReceivedMetrics[0]?.MessageBytesMax, m?.MessageBytesMax) && + Equals(responseReceivedMetrics[0]?.MessageBytesAverage, m?.MessageBytesAverage) && + Equals(responseReceivedMetrics[0]?.Messages, m?.Messages) && + Equals(responseReceivedMetrics[0]?.MessageConversionMin, m?.MessageConversionMin) && + Equals(responseReceivedMetrics[0]?.MessageConversionMax, m?.MessageConversionMax) && + Equals(responseReceivedMetrics[0]?.MessageConversionAverage, m?.MessageConversionAverage) && + Equals(responseReceivedMetrics[0]?.MessageConversionDuration, m?.MessageConversionDuration) + ))); + Assert.AreEqual(querySentMetrics[0]?.MessageBytes, queryReceivedMetrics[0]?.MessageBytes); + Assert.AreEqual(responseSentMetrics[0]?.MessageBytes, responseReceivedMetrics[0]?.MessageBytes); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + } +} diff --git a/AutomatedTesting/ContractConnectionTests/MiddleWareTests.cs b/AutomatedTesting/ContractConnectionTests/MiddleWareTests.cs new file mode 100644 index 0000000..ca27897 --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/MiddleWareTests.cs @@ -0,0 +1,274 @@ +using Moq; +using MQContract.Interfaces.Service; +using MQContract; +using AutomatedTesting.Messages; +using AutomatedTesting.ContractConnectionTests.Middlewares; +using MQContract.Interfaces.Middleware; +using MQContract.Interfaces; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class MiddleWareTests + { + [TestMethod] + public async Task TestRegisterGenericMiddleware() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + + var testMessage = new BasicMessage("testMessage"); + var messageChannel = "TestRegisterGenericMiddleware"; + + List messages = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .ReturnsAsync(transmissionResult); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .RegisterMiddleware(); + #endregion + + #region Act + _ = await contractConnection.PublishAsync(testMessage,channel:messageChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.AreEqual(messages[0].Channel, ChannelChangeMiddleware.ChangeChannel(messageChannel)); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestRegisterGenericMiddlewareThroughFunction() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + + var testMessage = new BasicMessage("testMessage"); + var messageChannel = "TestRegisterGenericMiddlewareThrouwFunction"; + var newChannel = "NewTestRegisterGenericMiddlewareThrouwFunction"; + var headers = new MessageHeader([ + new KeyValuePair("test","test") + ]); + + List messages = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .ReturnsAsync(transmissionResult); + + var mockMiddleware = new Mock(); + mockMiddleware.Setup(x => x.BeforeMessageEncodeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((IContext context, BasicMessage message, string? channel, MessageHeader messageHeader) => + { + return ValueTask.FromResult<(BasicMessage message, string? channel, MessageHeader messageHeader)>((message,newChannel,headers)); + }); + + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .RegisterMiddleware(() => mockMiddleware.Object); + #endregion + + #region Act + _ = await contractConnection.PublishAsync(testMessage, channel: messageChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.AreEqual(messages[0].Channel, newChannel); + Assert.AreEqual(1, messages[0].Header.Keys.Count()); + Assert.AreEqual(headers[headers.Keys.First()], messages[0].Header[messages[0].Header.Keys.First()]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockMiddleware.Verify(x => x.BeforeMessageEncodeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestRegisterSpecificTypeMiddleware() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + + var testMessage = new BasicMessage("testMessage"); + var messageChannel = "TestRegisterSpecificTypeMiddleware"; + + List messages = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .ReturnsAsync(transmissionResult); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .RegisterMiddleware(); + #endregion + + #region Act + _ = await contractConnection.PublishAsync(testMessage, channel: messageChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.AreEqual(messages[0].Channel, ChannelChangeMiddlewareForBasicMessage.ChangeChannel(messageChannel)); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestRegisterSpecificTypeMiddlewareThroughFunction() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + + var testMessage = new BasicMessage("testMessage"); + var messageChannel = "TestRegisterSpecificTypeMiddlewareThroughFunction"; + var newChannel = "NewTestRegisterSpecificTypeMiddlewareThroughFunction"; + var headers = new MessageHeader([ + new KeyValuePair("test","test") + ]); + + List messages = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .ReturnsAsync(transmissionResult); + + var mockMiddleware = new Mock>(); + mockMiddleware.Setup(x => x.BeforeMessageEncodeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((IContext context, BasicMessage message, string? channel, MessageHeader messageHeader) => + { + return ValueTask.FromResult<(BasicMessage message,string? channel,MessageHeader headers)>((message, newChannel, headers)); + }); + + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .RegisterMiddleware,BasicMessage>(() => mockMiddleware.Object); + #endregion + + #region Act + _ = await contractConnection.PublishAsync(testMessage, channel: messageChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.AreEqual(messages[0].Channel, newChannel); + Assert.AreEqual(1, messages[0].Header.Keys.Count()); + Assert.AreEqual(headers[headers.Keys.First()], messages[0].Header[messages[0].Header.Keys.First()]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockMiddleware.Verify(x => x.BeforeMessageEncodeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestRegisterGenericMiddlewareWithService() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + + var testMessage = new BasicMessage("testMessage"); + var messageChannel = "TestRegisterGenericMiddleware"; + var expectedChannel = "TestRegisterGenericMiddlewareWithService"; + + var services = Helper.ProduceServiceProvider(expectedChannel); + + List messages = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .ReturnsAsync(transmissionResult); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object, serviceProvider: services) + .RegisterMiddleware(); + #endregion + + #region Act + _ = await contractConnection.PublishAsync(testMessage, channel: messageChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.AreEqual(messages[0].Channel, expectedChannel); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestRegisterSpecificTypeMiddlewarePostDecodingThroughFunction() + { + #region Arrange + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + var serviceSubscription = new Mock(); + + var testMessage = new BasicMessage("testMessage"); + var messageChannel = "TestRegisterSpecificTypeMiddlewareThroughFunction"; + var headers = new MessageHeader([ + new KeyValuePair("test","test") + ]); + + var actions = new List>(); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Moq.Capture.In>(actions),It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + var rmessage = Helper.ProduceReceivedServiceMessage(message); + foreach (var act in actions) + act(rmessage); + return ValueTask.FromResult(transmissionResult); + }); + + var mockMiddleware = new Mock>(); + mockMiddleware.Setup(x => x.AfterMessageDecodeAsync(It.IsAny(),It.IsAny(),It.IsAny(),It.IsAny(),It.IsAny(),It.IsAny())) + .Returns((IContext context, BasicMessage message, string ID, MessageHeader messageHeader,DateTime recievedTimestamp,DateTime processedTimeStamp) => + { + return ValueTask.FromResult((message,headers)); + }); + + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .RegisterMiddleware, BasicMessage>(() => mockMiddleware.Object); + #endregion + + #region Act + var messages = new List>(); + _ = await contractConnection.SubscribeAsync((msg) => + { + messages.Add(msg); + return ValueTask.CompletedTask; + }, (error) => { }); + _ = await contractConnection.PublishAsync(testMessage, channel: messageChannel); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); + Assert.AreEqual(headers.Keys.Count(), messages[0].Headers.Keys.Count()); + Assert.AreEqual(headers[headers.Keys.First()], messages[0].Headers[messages[0].Headers.Keys.First()]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockMiddleware.Verify(x => x.AfterMessageDecodeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + } +} diff --git a/AutomatedTesting/ContractConnectionTests/Middlewares/ChannelChangeMiddleware.cs b/AutomatedTesting/ContractConnectionTests/Middlewares/ChannelChangeMiddleware.cs new file mode 100644 index 0000000..1e4d32d --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/Middlewares/ChannelChangeMiddleware.cs @@ -0,0 +1,12 @@ +using MQContract.Interfaces.Middleware; + +namespace AutomatedTesting.ContractConnectionTests.Middlewares +{ + internal class ChannelChangeMiddleware : IBeforeEncodeMiddleware + { + public static string ChangeChannel(string? channel) + => $"{channel}-Modified"; + public ValueTask<(T message, string? channel, MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context, T message, string? channel, MessageHeader messageHeader) + => ValueTask.FromResult<(T message, string? channel, MessageHeader messageHeader)>((message, ChangeChannel(channel), messageHeader)); + } +} diff --git a/AutomatedTesting/ContractConnectionTests/Middlewares/ChannelChangeMiddlewareForBasicMessage.cs b/AutomatedTesting/ContractConnectionTests/Middlewares/ChannelChangeMiddlewareForBasicMessage.cs new file mode 100644 index 0000000..86040d8 --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/Middlewares/ChannelChangeMiddlewareForBasicMessage.cs @@ -0,0 +1,13 @@ +using AutomatedTesting.Messages; +using MQContract.Interfaces.Middleware; + +namespace AutomatedTesting.ContractConnectionTests.Middlewares +{ + internal class ChannelChangeMiddlewareForBasicMessage : IBeforeEncodeSpecificTypeMiddleware + { + public static string ChangeChannel(string? channel) + => $"{channel}-ModifiedSpecifically"; + public ValueTask<(BasicMessage message, string? channel, MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context, BasicMessage message, string? channel, MessageHeader messageHeader) + => ValueTask.FromResult<(BasicMessage message, string? channel, MessageHeader messageHeader)>((message, ChangeChannel(channel), messageHeader)); + } +} diff --git a/AutomatedTesting/ContractConnectionTests/Middlewares/InjectedChannelChangeMiddleware.cs b/AutomatedTesting/ContractConnectionTests/Middlewares/InjectedChannelChangeMiddleware.cs new file mode 100644 index 0000000..3e6ec99 --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/Middlewares/InjectedChannelChangeMiddleware.cs @@ -0,0 +1,12 @@ +using AutomatedTesting.ServiceInjection; +using MQContract.Interfaces.Middleware; + +namespace AutomatedTesting.ContractConnectionTests.Middlewares +{ + internal class InjectedChannelChangeMiddleware(IInjectableService service) + : IBeforeEncodeMiddleware + { + public ValueTask<(T message, string? channel, MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context, T message, string? channel, MessageHeader messageHeader) + => ValueTask.FromResult<(T message, string? channel, MessageHeader messageHeader)>((message, service.Name, messageHeader)); + } +} diff --git a/AutomatedTesting/ContractConnectionTests/PingTests.cs b/AutomatedTesting/ContractConnectionTests/PingTests.cs index 8e510aa..ae0b86b 100644 --- a/AutomatedTesting/ContractConnectionTests/PingTests.cs +++ b/AutomatedTesting/ContractConnectionTests/PingTests.cs @@ -13,11 +13,11 @@ public async Task TestPingAsync() #region Arrange var pingResult = new PingResult("TestHost", "1.0.0", TimeSpan.FromSeconds(5)); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.PingAsync()) .ReturnsAsync(pingResult); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act diff --git a/AutomatedTesting/ContractConnectionTests/PublishTests.cs b/AutomatedTesting/ContractConnectionTests/PublishTests.cs index 2246aa5..3145e47 100644 --- a/AutomatedTesting/ContractConnectionTests/PublishTests.cs +++ b/AutomatedTesting/ContractConnectionTests/PublishTests.cs @@ -27,17 +27,14 @@ public async Task TestPublishAsyncWithNoExtendedAspects() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); - + List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - - var contractConnection = new ContractConnection(serviceConnection.Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -59,7 +56,7 @@ public async Task TestPublishAsyncWithNoExtendedAspects() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -74,10 +71,10 @@ public async Task TestPublishAsyncWithDifferentChannelName() List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -99,7 +96,7 @@ public async Task TestPublishAsyncWithDifferentChannelName() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -114,12 +111,12 @@ public async Task TestPublishAsyncWithMessageHeaders() List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); var messageHeader = new MessageHeader([new("testing", "testing")]); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -143,7 +140,7 @@ public async Task TestPublishAsyncWithMessageHeaders() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -154,19 +151,16 @@ public async Task TestPublishAsyncWithCompressionDueToMessageSize() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("AAAAAAAAAAAAAAAAAAAaaaaaaaaaaaaaaaaaaaa"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); serviceConnection.Setup(x => x.MaxMessageBodySize) .Returns(35); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -190,7 +184,7 @@ public async Task TestPublishAsyncWithCompressionDueToMessageSize() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -206,14 +200,14 @@ public async Task TestPublishAsyncWithGlobalEncoder() List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); var globalEncoder = new Mock(); - globalEncoder.Setup(x => x.Encode(It.IsAny())) - .Returns(encodedData); + globalEncoder.Setup(x => x.EncodeAsync(It.IsAny())) + .ReturnsAsync(encodedData); - var contractConnection = new ContractConnection(serviceConnection.Object, defaultMessageEncoder: globalEncoder.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, defaultMessageEncoder: globalEncoder.Object); #endregion #region Act @@ -235,8 +229,8 @@ public async Task TestPublishAsyncWithGlobalEncoder() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - globalEncoder.Verify(x => x.Encode(It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + globalEncoder.Verify(x => x.EncodeAsync(It.IsAny()), Times.Once); #endregion } @@ -255,14 +249,14 @@ public async Task TestPublishAsyncWithGlobalEncryptor() ]); var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); var globalEncryptor = new Mock(); - globalEncryptor.Setup(x => x.Encrypt(Capture.In(binaries), out headers)) - .Returns((byte[] binary, Dictionary h) => binary.Reverse().ToArray()); + globalEncryptor.Setup(x => x.EncryptAsync(Capture.In(binaries), out headers)) + .ReturnsAsync((byte[] binary, Dictionary h) => binary.Reverse().ToArray()); - var contractConnection = new ContractConnection(serviceConnection.Object, defaultMessageEncryptor: globalEncryptor.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, defaultMessageEncryptor: globalEncryptor.Object); #endregion #region Act @@ -286,56 +280,8 @@ public async Task TestPublishAsyncWithGlobalEncryptor() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - globalEncryptor.Verify(x => x.Encrypt(It.IsAny(), out headers), Times.Once); - #endregion - } - - [TestMethod] - public async Task TestPublishAsyncWithServiceChannelOptions() - { - #region Arrange - var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); - - var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); - var serviceChannelOptions = new TestServiceChannelOptions("PublishAsync"); - - List messages = []; - List options = []; - - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), Capture.In(options), It.IsAny())) - .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - - var contractConnection = new ContractConnection(serviceConnection.Object); - #endregion - - #region Act - var stopwatch = Stopwatch.StartNew(); - var result = await contractConnection.PublishAsync(testMessage, options: serviceChannelOptions); - stopwatch.Stop(); - System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); - #endregion - - #region Assert - Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); - Assert.IsNotNull(result); - Assert.AreEqual(transmissionResult, result); - Assert.AreEqual(typeof(BasicMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); - Assert.AreEqual(0, messages[0].Header.Keys.Count()); - Assert.AreEqual("U-BasicMessage-0.0.0.0", messages[0].MessageTypeID); - Assert.IsTrue(messages[0].Data.Length>0); - Assert.AreEqual(testMessage, JsonSerializer.Deserialize(new MemoryStream(messages[0].Data.ToArray()))); - Assert.AreEqual(1, options.Count); - Assert.IsInstanceOfType(options[0]); - Assert.AreEqual(serviceChannelOptions, options[0]); - #endregion - - #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + globalEncryptor.Verify(x => x.EncryptAsync(It.IsAny(), out headers), Times.Once); #endregion } @@ -346,17 +292,14 @@ public async Task TestPublishAsyncWithNamedAndVersionedMessage() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new NamedAndVersionedMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -378,7 +321,7 @@ public async Task TestPublishAsyncWithNamedAndVersionedMessage() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -389,17 +332,14 @@ public async Task TestPublishAsyncWithMessageWithDefinedEncoder() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new CustomEncoderMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages),It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -417,11 +357,11 @@ public async Task TestPublishAsyncWithMessageWithDefinedEncoder() Assert.AreEqual(0, messages[0].Header.Keys.Count()); Assert.AreEqual("U-CustomEncoderMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); - Assert.AreEqual(testMessage, new TestMessageEncoder().Decode(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(testMessage, await new TestMessageEncoder().DecodeAsync(new MemoryStream(messages[0].Data.ToArray()))); #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -433,19 +373,16 @@ public async Task TestPublishAsyncWithMessageWithDefinedServiceInjectableEncoder var testMessage = new CustomEncoderWithInjectionMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); var serviceName = "TestPublishAsyncWithMessageWithDefinedServiceInjectableEncoder"; var services = Helper.ProduceServiceProvider(serviceName); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object, serviceProvider: services); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, serviceProvider: services); #endregion #region Act @@ -464,12 +401,12 @@ public async Task TestPublishAsyncWithMessageWithDefinedServiceInjectableEncoder Assert.AreEqual("U-CustomEncoderWithInjectionMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); Assert.AreEqual(testMessage, - new TestMessageEncoderWithInjection(services.GetRequiredService()).Decode(new MemoryStream(messages[0].Data.ToArray())) + await new TestMessageEncoderWithInjection(services.GetRequiredService()).DecodeAsync(new MemoryStream(messages[0].Data.ToArray())) ); #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -480,17 +417,14 @@ public async Task TestPublishAsyncWithMessageWithDefinedEncryptor() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new CustomEncryptorMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -507,12 +441,12 @@ public async Task TestPublishAsyncWithMessageWithDefinedEncryptor() Assert.AreEqual(typeof(CustomEncryptorMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); Assert.AreEqual("U-CustomEncryptorMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); - var decodedData = new TestMessageEncryptor().Decrypt(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); + var decodedData = await new TestMessageEncryptor().DecryptAsync(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(decodedData)); #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -523,19 +457,16 @@ public async Task TestPublishAsyncWithMessageWithDefinedServiceInjectableEncrypt var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new CustomEncryptorWithInjectionMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); var serviceName = "TestPublishAsyncWithMessageWithDefinedServiceInjectableEncryptor"; var services = Helper.ProduceServiceProvider(serviceName); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object, serviceProvider: services); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, serviceProvider: services); #endregion #region Act @@ -552,12 +483,12 @@ public async Task TestPublishAsyncWithMessageWithDefinedServiceInjectableEncrypt Assert.AreEqual(typeof(CustomEncryptorWithInjectionMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); Assert.AreEqual("U-CustomEncryptorWithInjectionMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); - var decodedData = new TestMessageEncryptorWithInjection(services.GetRequiredService()).Decrypt(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); + var decodedData = await new TestMessageEncryptorWithInjection(services.GetRequiredService()).DecryptAsync(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(decodedData)); #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -568,22 +499,19 @@ public async Task TestPublishAsyncWithNoMessageChannelThrowsError() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new NoChannelMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act var stopwatch = Stopwatch.StartNew(); - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.PublishAsync(testMessage)); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.PublishAsync(testMessage)); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); #endregion @@ -595,7 +523,7 @@ public async Task TestPublishAsyncWithNoMessageChannelThrowsError() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Never); #endregion } @@ -606,24 +534,21 @@ public async Task TestPublishAsyncWithToLargeAMessageThrowsError() var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); var testMessage = new BasicMessage("testMessage"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); serviceConnection.Setup(x => x.MaxMessageBodySize) .Returns(1); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act var stopwatch = Stopwatch.StartNew(); - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.PublishAsync(testMessage)); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.PublishAsync(testMessage)); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); #endregion @@ -635,7 +560,7 @@ public async Task TestPublishAsyncWithToLargeAMessageThrowsError() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Never); #endregion } @@ -647,17 +572,14 @@ public async Task TestPublishAsyncWithTwoDifferentMessageTypes() var testMessage1 = new BasicMessage("testMessage1"); var testMessage2 = new NoChannelMessage("testMessage2"); - var defaultTimeout = TimeSpan.FromMinutes(1); List messages = []; var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) .ReturnsAsync(transmissionResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -691,7 +613,7 @@ public async Task TestPublishAsyncWithTwoDifferentMessageTypes() #endregion #region Verify - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } } diff --git a/AutomatedTesting/ContractConnectionTests/QueryInboxTests.cs b/AutomatedTesting/ContractConnectionTests/QueryInboxTests.cs new file mode 100644 index 0000000..06c9354 --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/QueryInboxTests.cs @@ -0,0 +1,182 @@ +using AutomatedTesting.Messages; +using Moq; +using MQContract.Attributes; +using MQContract.Interfaces.Service; +using MQContract; +using System.Diagnostics; +using System.Text.Json; +using System.Reflection; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class QueryInboxTests + { + [TestMethod] + public async Task TestQueryAsyncWithNoExtendedAspects() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); + + var mockSubscription = new Mock(); + + var defaultTimeout = TimeSpan.FromMinutes(1); + + List> receivedActions = []; + List messages = []; + List messageIDs = []; + var acknowledgeCount = 0; + + + var serviceConnection = new Mock(); + serviceConnection.Setup(x=>x.EstablishInboxSubscriptionAsync(Capture.In>(receivedActions),It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(messageIDs), It.IsAny())) + .Returns((ServiceMessage message,Guid messageID, CancellationToken cancellationToken) => + { + foreach (var action in receivedActions) + action(new( + queryResult.ID, + queryResult.MessageTypeID, + message.Channel, + queryResult.Header, + messageID, + queryResult.Data, + () => { + acknowledgeCount++; + return ValueTask.CompletedTask; + } + )); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + serviceConnection.Setup(x => x.DefaultTimeout) + .Returns(defaultTimeout); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(testMessage); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + await contractConnection.CloseAsync(); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result); + Assert.AreEqual(queryResult.ID, result.ID); + Assert.IsNull(result.Error); + Assert.IsFalse(result.IsError); + Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); + Assert.AreEqual(1, messageIDs.Count); + Assert.AreEqual(0, messages[0].Header.Keys.Count()); + Assert.AreEqual("U-BasicQueryMessage-0.0.0.0", messages[0].MessageTypeID); + Assert.IsTrue(messages[0].Data.Length>0); + Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(responseMessage, result.Result); + #endregion + + #region Verify + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.EstablishInboxSubscriptionAsync(It.IsAny>(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithErrorInPublish() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var errorMessage = "Unable to transmit"; + + var mockSubscription = new Mock(); + + var defaultTimeout = TimeSpan.FromMinutes(1); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.EstablishInboxSubscriptionAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, Guid messageID, CancellationToken cancellationToken) => + { + return ValueTask.FromResult(new TransmissionResult(message.ID,errorMessage)); + }); + serviceConnection.Setup(x => x.DefaultTimeout) + .Returns(defaultTimeout); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage)); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + await contractConnection.CloseAsync(); + #endregion + + #region Assert + Assert.IsNotNull(exception); + Assert.AreEqual(errorMessage, exception.Message); + #endregion + + #region Verify + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.EstablishInboxSubscriptionAsync(It.IsAny>(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithTimeout() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + + var mockSubscription = new Mock(); + + var defaultTimeout = TimeSpan.FromSeconds(5); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.EstablishInboxSubscriptionAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, Guid messageID, CancellationToken cancellationToken) => + { + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + serviceConnection.Setup(x => x.DefaultTimeout) + .Returns(defaultTimeout); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage)); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + await contractConnection.CloseAsync(); + #endregion + + #region Assert + Assert.IsNotNull(exception); + #endregion + + #region Verify + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.EstablishInboxSubscriptionAsync(It.IsAny>(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + } +} diff --git a/AutomatedTesting/ContractConnectionTests/QueryTests.cs b/AutomatedTesting/ContractConnectionTests/QueryTests.cs index 8665193..f0e79e2 100644 --- a/AutomatedTesting/ContractConnectionTests/QueryTests.cs +++ b/AutomatedTesting/ContractConnectionTests/QueryTests.cs @@ -38,13 +38,13 @@ public async Task TestQueryAsyncWithNoExtendedAspects() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -71,7 +71,7 @@ public async Task TestQueryAsyncWithNoExtendedAspects() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -93,13 +93,13 @@ public async Task TestQueryAsyncWithDifferentChannelName() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -126,7 +126,7 @@ public async Task TestQueryAsyncWithDifferentChannelName() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -148,15 +148,15 @@ public async Task TestQueryAsyncWithMessageHeaders() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var messageHeader = new MessageHeader([new("testing", "testing")]); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -185,7 +185,7 @@ public async Task TestQueryAsyncWithMessageHeaders() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -208,13 +208,13 @@ public async Task TestQueryAsyncWithTimeout() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -241,7 +241,7 @@ public async Task TestQueryAsyncWithTimeout() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -263,15 +263,15 @@ public async Task TestQueryAsyncWithCompressionDueToMessageSize() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); serviceConnection.Setup(x => x.MaxMessageBodySize) .Returns(37); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -300,7 +300,7 @@ public async Task TestQueryAsyncWithCompressionDueToMessageSize() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -321,24 +321,24 @@ public async Task TestQueryAsyncWithGlobalEncoder() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var globalEncoder = new Mock(); - globalEncoder.Setup(x => x.Encode(It.IsAny())) - .Returns(encodedData); - globalEncoder.Setup(x => x.Decode(It.IsAny())) - .Returns((Stream str) => + globalEncoder.Setup(x => x.EncodeAsync(It.IsAny())) + .ReturnsAsync(encodedData); + globalEncoder.Setup(x => x.DecodeAsync(It.IsAny())) + .ReturnsAsync((Stream str) => { var reader = new StreamReader(str); var result = new BasicResponseMessage(reader.ReadToEnd()); return result; }); - var contractConnection = new ContractConnection(serviceConnection.Object, defaultMessageEncoder: globalEncoder.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, defaultMessageEncoder: globalEncoder.Object); #endregion #region Act @@ -365,7 +365,7 @@ public async Task TestQueryAsyncWithGlobalEncoder() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -391,24 +391,24 @@ public async Task TestQueryAsyncWithGlobalEncryptor() new KeyValuePair("test","test") ]); - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); var globalEncryptor = new Mock(); - globalEncryptor.Setup(x => x.Encrypt(Capture.In(binaries), out headers)) - .Returns((byte[] binary, Dictionary h) => binary.Reverse().ToArray()); - globalEncryptor.Setup(x => x.Decrypt(It.IsAny(), It.IsAny())) - .Returns((Stream source, MessageHeader headers) => + globalEncryptor.Setup(x => x.EncryptAsync(Capture.In(binaries), out headers)) + .ReturnsAsync((byte[] binary, Dictionary h) => binary.Reverse().ToArray()); + globalEncryptor.Setup(x => x.DecryptAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Stream source, MessageHeader headers) => { var buff = new byte[source.Length]; source.Read(buff, 0, buff.Length); return new MemoryStream(buff.Reverse().ToArray()); }); - var contractConnection = new ContractConnection(serviceConnection.Object, defaultMessageEncryptor: globalEncryptor.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, defaultMessageEncryptor: globalEncryptor.Object); #endregion #region Act @@ -437,7 +437,7 @@ public async Task TestQueryAsyncWithGlobalEncryptor() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -459,13 +459,13 @@ public async Task TestQueryAsyncWithTimeoutAttribute() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -492,67 +492,7 @@ public async Task TestQueryAsyncWithTimeoutAttribute() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - #endregion - } - - [TestMethod] - public async Task TestQueryAsyncWithServiceChannelOptions() - { - #region Arrange - var testMessage = new BasicQueryMessage("testMessage"); - var responseMessage = new BasicResponseMessage("testResponse"); - using var ms = new MemoryStream(); - await JsonSerializer.SerializeAsync(ms, responseMessage); - var responseData = (ReadOnlyMemory)ms.ToArray(); - - var queryResult = new ServiceQueryResult(Guid.NewGuid().ToString(), new MessageHeader([]), "U-BasicResponseMessage-0.0.0.0", responseData); - - - var defaultTimeout = TimeSpan.FromMinutes(1); - - List messages = []; - List timeouts = []; - List options = []; - var serviceChannelOptions = new TestServiceChannelOptions("QWueryAsync"); - - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), Capture.In(options), It.IsAny())) - .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) - .Returns(defaultTimeout); - - var contractConnection = new ContractConnection(serviceConnection.Object); - #endregion - - #region Act - var stopwatch = Stopwatch.StartNew(); - var result = await contractConnection.QueryAsync(testMessage, options: serviceChannelOptions); - stopwatch.Stop(); - System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); - #endregion - - #region Assert - Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); - Assert.IsNotNull(result); - Assert.AreEqual(queryResult.ID, result.ID); - Assert.IsNull(result.Error); - Assert.IsFalse(result.IsError); - Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); - Assert.AreEqual(1, timeouts.Count); - Assert.AreEqual(defaultTimeout, timeouts[0]); - Assert.AreEqual(0, messages[0].Header.Keys.Count()); - Assert.AreEqual("U-BasicQueryMessage-0.0.0.0", messages[0].MessageTypeID); - Assert.IsTrue(messages[0].Data.Length>0); - Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(new MemoryStream(messages[0].Data.ToArray()))); - Assert.AreEqual(responseMessage, result.Result); - Assert.AreEqual(1, options.Count); - Assert.IsInstanceOfType(options[0]); - Assert.AreEqual(serviceChannelOptions, options[0]); - #endregion - - #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -574,13 +514,13 @@ public async Task TestQueryAsyncWithNamedAndVersionedMessage() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -607,7 +547,7 @@ public async Task TestQueryAsyncWithNamedAndVersionedMessage() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -629,13 +569,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedEncoder() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -657,12 +597,12 @@ public async Task TestQueryAsyncWithMessageWithDefinedEncoder() Assert.AreEqual(0, messages[0].Header.Keys.Count()); Assert.AreEqual("U-CustomEncoderMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); - Assert.AreEqual(testMessage, new TestMessageEncoder().Decode(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(testMessage, await new TestMessageEncoder().DecodeAsync(new MemoryStream(messages[0].Data.ToArray()))); Assert.AreEqual(responseMessage, result.Result); #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -686,13 +626,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedServiceInjectableEncoder() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object, serviceProvider: services); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, serviceProvider: services); #endregion #region Act @@ -715,13 +655,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedServiceInjectableEncoder() Assert.AreEqual("U-CustomEncoderWithInjectionMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); Assert.AreEqual(testMessage, - new TestMessageEncoderWithInjection(services.GetRequiredService()).Decode(new MemoryStream(messages[0].Data.ToArray())) + await new TestMessageEncoderWithInjection(services.GetRequiredService()).DecodeAsync(new MemoryStream(messages[0].Data.ToArray())) ); Assert.AreEqual(responseMessage, result.Result); #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -743,13 +683,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedEncryptor() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -770,13 +710,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedEncryptor() Assert.AreEqual(defaultTimeout, timeouts[0]); Assert.AreEqual("U-CustomEncryptorMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); - var decodedData = new TestMessageEncryptor().Decrypt(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); + var decodedData = await new TestMessageEncryptor().DecryptAsync(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(decodedData)); Assert.AreEqual(responseMessage, result.Result); #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -800,13 +740,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedServiceInjectableEncryptor List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object, serviceProvider: services); + var contractConnection = ContractConnection.Instance(serviceConnection.Object, serviceProvider: services); #endregion #region Act @@ -827,13 +767,13 @@ public async Task TestQueryAsyncWithMessageWithDefinedServiceInjectableEncryptor Assert.AreEqual(defaultTimeout, timeouts[0]); Assert.AreEqual("U-CustomEncryptorWithInjectionMessage-0.0.0.0", messages[0].MessageTypeID); Assert.IsTrue(messages[0].Data.Length>0); - var decodedData = new TestMessageEncryptorWithInjection(services.GetRequiredService()).Decrypt(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); + var decodedData = await new TestMessageEncryptorWithInjection(services.GetRequiredService()).DecryptAsync(new MemoryStream(messages[0].Data.ToArray()), messages[0].Header); Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(decodedData)); Assert.AreEqual(responseMessage, result.Result); #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -855,18 +795,18 @@ public async Task TestQueryAsyncWithNoMessageChannelThrowsError() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act var stopwatch = Stopwatch.StartNew(); - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.QueryAsync(testMessage)); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage)); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); #endregion @@ -878,7 +818,7 @@ public async Task TestQueryAsyncWithNoMessageChannelThrowsError() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); #endregion } @@ -900,20 +840,20 @@ public async Task TestQueryAsyncWithToLargeAMessageThrowsError() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); serviceConnection.Setup(x => x.MaxMessageBodySize) .Returns(1); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act var stopwatch = Stopwatch.StartNew(); - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.QueryAsync(testMessage)); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage)); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); #endregion @@ -925,7 +865,7 @@ public async Task TestQueryAsyncWithToLargeAMessageThrowsError() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); #endregion } @@ -948,13 +888,13 @@ public async Task TestQueryAsyncWithTwoDifferentMessageTypes() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -997,7 +937,7 @@ public async Task TestQueryAsyncWithTwoDifferentMessageTypes() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -1019,13 +959,13 @@ public async Task TestQueryAsyncWithAttributeReturnType() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act @@ -1052,7 +992,37 @@ public async Task TestQueryAsyncWithAttributeReturnType() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithAttributeReturnTypeThrowingTimeoutException() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + + var defaultTimeout = TimeSpan.FromMinutes(1); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(); + serviceConnection.Setup(x => x.DefaultTimeout) + .Returns(defaultTimeout); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var exception = await Assert.ThrowsExceptionAsync(async ()=>await contractConnection.QueryAsync(testMessage)); + #endregion + + #region Assert + Assert.IsNotNull(exception); + #endregion + + #region Verify + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -1074,18 +1044,18 @@ public async Task TestQueryAsyncWithNoReturnType() List messages = []; List timeouts = []; - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny(), It.IsAny())) + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.QueryAsync(Capture.In(messages), Capture.In(timeouts), It.IsAny())) .ReturnsAsync(queryResult); - serviceConnection.Setup(x => x.DefaultTimout) + serviceConnection.Setup(x => x.DefaultTimeout) .Returns(defaultTimeout); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act var stopwatch = Stopwatch.StartNew(); - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.QueryAsync(testMessage)); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage)); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); #endregion @@ -1097,7 +1067,7 @@ public async Task TestQueryAsyncWithNoReturnType() #endregion #region Verify - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); #endregion } } diff --git a/AutomatedTesting/ContractConnectionTests/QueryWithoutQueryResponseTests.cs b/AutomatedTesting/ContractConnectionTests/QueryWithoutQueryResponseTests.cs new file mode 100644 index 0000000..31169a5 --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/QueryWithoutQueryResponseTests.cs @@ -0,0 +1,396 @@ +using AutomatedTesting.Messages; +using Moq; +using MQContract.Attributes; +using MQContract.Interfaces.Service; +using MQContract; +using System.Diagnostics; +using System.Text.Json; +using System.Reflection; +using Castle.Core.Internal; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class QueryWithoutQueryResponseTests + { + private const string REPLY_CHANNEL_HEADER = "_QueryReplyChannel"; + private const string QUERY_IDENTIFIER_HEADER = "_QueryClientID"; + private const string REPLY_ID = "_QueryReplyID"; + + [TestMethod] + public async Task TestQueryAsyncWithNoExtendedAspects() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + var responseChannel = "BasicQuery.Response"; + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + + var mockSubscription = new Mock(); + + List messages = []; + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + Capture.In(channels), It.IsAny(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + messages.Add(message); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", responseChannel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result); + Assert.IsNull(result.Error); + Assert.IsFalse(result.IsError); + Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); + Assert.AreEqual(1, channels.Count); + Assert.AreEqual(responseChannel, channels[0]); + Assert.AreEqual(1, messages.Count); + Assert.IsTrue(messages[0].Data.Length>0); + Assert.AreEqual(3, messages[0].Header.Keys.Count()); + Assert.AreEqual(responseChannel, messages[0].Header[REPLY_CHANNEL_HEADER]); + Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(responseMessage, result.Result); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithAHeaderValue() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + var responseChannel = "BasicQuery.Response"; + var headerKey = "MyHeaderKey"; + var headerValue = "MyHeaderValue"; + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var mockSubscription = new Mock(); + + List messages = []; + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + Capture.In(channels), It.IsAny(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + messages.Add(message); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", responseChannel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(testMessage, messageHeader: new([new KeyValuePair(headerKey, headerValue)]), responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result); + Assert.IsNull(result.Error); + Assert.IsFalse(result.IsError); + Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); + Assert.AreEqual(1, channels.Count); + Assert.AreEqual(responseChannel, channels[0]); + Assert.AreEqual(1, messages.Count); + Assert.IsTrue(messages[0].Data.Length>0); + Assert.AreEqual(4, messages[0].Header.Keys.Count()); + Assert.AreEqual(responseChannel, messages[0].Header[REPLY_CHANNEL_HEADER]); + Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(responseMessage, result.Result); + Assert.AreEqual(1, result.Header.Keys.Count()); + Assert.AreEqual(headerValue, result.Header[headerKey]); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithASecondMessageNotMatchingTheRequestBeingSent() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + var responseChannel = "BasicQuery.Response"; + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var mockSubscription = new Mock(); + + List messages = []; + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + Capture.In(channels), It.IsAny(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + messages.Add(message); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", responseChannel, new([ + new KeyValuePair(QUERY_IDENTIFIER_HEADER,Guid.NewGuid().ToString()), + new KeyValuePair(REPLY_ID,Guid.NewGuid().ToString()), + new KeyValuePair(REPLY_CHANNEL_HEADER,responseChannel) + ]), responseData); + foreach (var action in messageActions) + action(resp); + resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", responseChannel, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result); + Assert.IsNull(result.Error); + Assert.IsFalse(result.IsError); + Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); + Assert.AreEqual(1, channels.Count); + Assert.AreEqual(responseChannel, channels[0]); + Assert.AreEqual(1, messages.Count); + Assert.IsTrue(messages[0].Data.Length>0); + Assert.AreEqual(3, messages[0].Header.Keys.Count()); + Assert.AreEqual(responseChannel, messages[0].Header[REPLY_CHANNEL_HEADER]); + Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(responseMessage, result.Result); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithTheAttributeChannel() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseMessage = new BasicResponseMessage("testResponse"); + var responseChannel = typeof(BasicQueryMessage).GetAttribute()?.Name; + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, responseMessage); + var responseData = (ReadOnlyMemory)ms.ToArray(); + + var mockSubscription = new Mock(); + + List messages = []; + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + Capture.In(channels), It.IsAny(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + messages.Add(message); + var resp = new ReceivedServiceMessage(message.ID, "U-BasicResponseMessage-0.0.0.0", responseChannel!, message.Header, responseData); + foreach (var action in messageActions) + action(resp); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(testMessage); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(result); + Assert.IsNull(result.Error); + Assert.IsFalse(result.IsError); + Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, messages[0].Channel); + Assert.AreEqual(1, channels.Count); + Assert.AreEqual(responseChannel, channels[0]); + Assert.AreEqual(1, messages.Count); + Assert.IsTrue(messages[0].Data.Length>0); + Assert.AreEqual(3, messages[0].Header.Keys.Count()); + Assert.AreEqual(responseChannel, messages[0].Header[REPLY_CHANNEL_HEADER]); + Assert.AreEqual(testMessage, await JsonSerializer.DeserializeAsync(new MemoryStream(messages[0].Data.ToArray()))); + Assert.AreEqual(responseMessage, result.Result); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncFailingToCreateSubscription() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseChannel = "BasicQuery.Response"; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ValueTask.FromResult(null)); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var error = await Assert.ThrowsExceptionAsync(async()=> await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel)); + #endregion + + #region Assert + Assert.IsNotNull(error); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncFailingWithNoResponseChannel() + { + #region Arrange + var testMessage = new BasicResponseMessage("testMessage"); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ValueTask.FromResult(null)); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var error = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage, channel: "Test")); + #endregion + + #region Assert + Assert.IsNotNull(error); + Assert.AreEqual("responseChannel", error.ParamName); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + #endregion + } + + [TestMethod] + public async Task TestQueryAsyncWithTimeoutException() + { + #region Arrange + var testMessage = new BasicQueryMessage("testMessage"); + var responseChannel = "BasicQuery.Response"; + var defaultTimeout = TimeSpan.FromSeconds(5); + + var mockSubscription = new Mock(); + + List messages = []; + List> messageActions = []; + List channels = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + Capture.In(channels), It.IsAny(), It.IsAny())) + .ReturnsAsync(mockSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + messages.Add(message); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var error = await Assert.ThrowsExceptionAsync(async () => await contractConnection.QueryAsync(testMessage, responseChannel: responseChannel, timeout: defaultTimeout)); + #endregion + + #region Assert + Assert.IsNotNull(error); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + mockSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } + } +} diff --git a/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseTests.cs b/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseTests.cs index 376ff4b..cb63750 100644 --- a/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseTests.cs +++ b/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseTests.cs @@ -6,6 +6,7 @@ using MQContract; using System.Diagnostics; using System.Reflection; +using MQContract.Interfaces.Encoding; namespace AutomatedTesting.ContractConnectionTests { @@ -18,31 +19,29 @@ public async Task TestSubscribeQueryResponseAsyncWithNoExtendedAspects() #region Arrange var serviceSubscription = new Mock(); - var recievedActions = new List>>(); + var receivedActions = new List>>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - Capture.In>>(recievedActions), + Capture.In>>(receivedActions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), - It.IsAny(), - It.IsAny())) + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (ServiceMessage message, TimeSpan timeout, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); - var result = await recievedActions[0](rmessage); + var result = await receivedActions[0](rmessage); return Helper.ProduceQueryResult(result); }); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); var message = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); var responseMessage = new BasicResponseMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); @@ -50,11 +49,12 @@ public async Task TestSubscribeQueryResponseAsyncWithNoExtendedAspects() #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { messages.Add(msg); - return Task.FromResult(new QueryResponseMessage(responseMessage,null)); + return ValueTask.FromResult(new QueryResponseMessage(responseMessage, null)); }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.QueryAsync(message); @@ -66,32 +66,31 @@ public async Task TestSubscribeQueryResponseAsyncWithNoExtendedAspects() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result); - Assert.AreEqual(1, recievedActions.Count); + Assert.AreEqual(1, receivedActions.Count); Assert.AreEqual(1, channels.Count); Assert.AreEqual(1, groups.Count); Assert.AreEqual(1, serviceMessages.Count); Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message, messages[0].Message); Assert.AreEqual(exception, exceptions[0]); Assert.IsFalse(result.IsError); Assert.IsNull(result.Error); Assert.AreEqual(result.Result, responseMessage); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -104,29 +103,27 @@ public async Task TestSubscribeQueryResponseAsyncWithSpecificChannel() var channels = new List(); var channelName = "TestSubscribeQueryResponseWithSpecificChannel"; - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), Capture.In(channels), - It.IsAny(), - It.IsAny(), - It.IsAny())) + It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var subscription1 = await contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var subscription1 = await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { }, - channel:channelName); + channel: channelName); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var subscription2 = await contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var subscription2 = await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { }, channel: channelName); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. @@ -141,8 +138,7 @@ public async Task TestSubscribeQueryResponseAsyncWithSpecificChannel() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -155,28 +151,26 @@ public async Task TestSubscribeQueryResponseAsyncWithSpecificGroup() var groups = new List(); var groupName = "TestSubscribeQueryResponseWithSpecificGroup"; - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), It.IsAny(), - Capture.In(groups), - It.IsAny(), - It.IsAny())) + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var subscription1 = await contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var subscription1 = await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { }, group: groupName); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var subscription2 = await contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var subscription2 = await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { }); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. #endregion @@ -190,51 +184,7 @@ public async Task TestSubscribeQueryResponseAsyncWithSpecificGroup() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); - #endregion - } - - [TestMethod] - public async Task TestSubscribeQueryResponseAsyncWithServiceChannelOptions() - { - #region Arrange - var serviceSubscription = new Mock(); - - var serviceChannelOptions = new TestServiceChannelOptions("TestSubscribeQueryResponseWithServiceChannelOptions"); - List options = []; - - var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), - It.IsAny>(), - It.IsAny(), - It.IsAny(), - Capture.In(options), - It.IsAny())) - .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); - #endregion - - #region Act -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var subscription = await contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), - (error) => { }, - options:serviceChannelOptions); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - #endregion - - #region Assert - Assert.IsNotNull(subscription); - Assert.AreEqual(1, options.Count); - Assert.IsInstanceOfType(options[0]); - Assert.AreEqual(serviceChannelOptions, options[0]); - #endregion - - #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -244,22 +194,20 @@ public async Task TestSubscribeQueryResponseAsyncNoMessageChannelThrowsError() #region Arrange var serviceSubscription = new Mock(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) + It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { }) ); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. @@ -272,8 +220,7 @@ public async Task TestSubscribeQueryResponseAsyncNoMessageChannelThrowsError() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); #endregion } @@ -281,22 +228,20 @@ public async Task TestSubscribeQueryResponseAsyncNoMessageChannelThrowsError() public async Task TestSubscribeQueryResponseAsyncReturnFailedSubscription() { #region Arrange - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) + It.IsAny(), It.IsAny())) .ReturnsAsync((IServiceSubscription?)null); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { }) ); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. @@ -307,8 +252,7 @@ public async Task TestSubscribeQueryResponseAsyncReturnFailedSubscription() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -318,24 +262,22 @@ public async Task TestSubscribeQueryResponseAsyncCleanup() #region Arrange var serviceSubscription = new Mock(); serviceSubscription.Setup(x => x.EndAsync()) - .Returns(Task.CompletedTask); + .Returns(ValueTask.CompletedTask); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - It.IsAny>>(), + It.IsAny>>(), It.IsAny>(), It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) + It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - var subscription = await contractConnection.SubscribeQueryResponseAsync( - (msg) => Task.FromResult>(null), + var subscription = await contractConnection.SubscribeQueryAsyncResponseAsync( + (msg) => ValueTask.FromResult>(null), (error) => { } ); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. @@ -347,8 +289,7 @@ public async Task TestSubscribeQueryResponseAsyncCleanup() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); serviceSubscription.Verify(x => x.EndAsync(), Times.Once); #endregion } @@ -359,31 +300,29 @@ public async Task TestSubscribeQueryResponseAsyncWithSynchronousActions() #region Arrange var serviceSubscription = new Mock(); - var recievedActions = new List>>(); + var receivedActions = new List>>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - Capture.In>>(recievedActions), + Capture.In>>(receivedActions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), - It.IsAny(), - It.IsAny())) + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (ServiceMessage message, TimeSpan timeout, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); - var result = await recievedActions[0](rmessage); + var result = await receivedActions[0](rmessage); return Helper.ProduceQueryResult(result); }); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); var message1 = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects1"); var message2 = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects2"); @@ -392,13 +331,13 @@ public async Task TestSubscribeQueryResponseAsyncWithSynchronousActions() #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => + { messages.Add(msg); - return Task.FromResult(new QueryResponseMessage(responseMessage, null)); - }, (error) => exceptions.Add(error), - synchronous:true); + return new(responseMessage, null); + }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result1 = await contractConnection.QueryAsync(message1); stopwatch.Stop(); @@ -413,20 +352,20 @@ public async Task TestSubscribeQueryResponseAsyncWithSynchronousActions() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 2, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 2, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result1); - Assert.AreEqual(1, recievedActions.Count); + Assert.AreEqual(1, receivedActions.Count); Assert.AreEqual(1, channels.Count); Assert.AreEqual(1, groups.Count); Assert.AreEqual(2, serviceMessages.Count); Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message1, messages[0].Message); Assert.AreEqual(message2, messages[1].Message); Assert.AreEqual(exception, exceptions[0]); @@ -436,13 +375,12 @@ public async Task TestSubscribeQueryResponseAsyncWithSynchronousActions() Assert.IsFalse(result2.IsError); Assert.IsNull(result2.Error); Assert.AreEqual(result2.Result, responseMessage); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -452,41 +390,39 @@ public async Task TestSubscribeQueryResponseAsyncErrorTriggeringInOurAction() #region Arrange var serviceSubscription = new Mock(); - var recievedActions = new List>>(); + var receivedActions = new List>>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - var serviceConnection = new Mock(); + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - Capture.In>>(recievedActions), + Capture.In>>(receivedActions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), - It.IsAny(), - It.IsAny())) + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (ServiceMessage message, TimeSpan timeout, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); - var result = await recievedActions[0](rmessage); + var result = await receivedActions[0](rmessage); return Helper.ProduceQueryResult(result); }); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); var message = new BasicQueryMessage("TestSubscribeQueryResponseAsyncErrorTriggeringInOurAction"); - var responseMessage = new BasicResponseMessage("TestSubscribeQueryResponseAsyncErrorTriggeringInOurAction"); var exception = new NullReferenceException("TestSubscribeQueryResponseAsyncErrorTriggeringInOurAction"); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => { + var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => + { throw exception; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); @@ -499,116 +435,228 @@ public async Task TestSubscribeQueryResponseAsyncErrorTriggeringInOurAction() Assert.IsTrue(await Helper.WaitForCount(exceptions, 1, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result); - Assert.AreEqual(1, recievedActions.Count); + Assert.AreEqual(1, receivedActions.Count); Assert.AreEqual(1, channels.Count); Assert.AreEqual(1, groups.Count); Assert.AreEqual(1, serviceMessages.Count); Assert.AreEqual(0, messages.Count); Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(exception, exceptions[0]); Assert.IsTrue(result.IsError); Assert.AreEqual(exception.Message, result.Error); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } [TestMethod] - public async Task TestSubscribeQueryResponseAsyncWithDisposal() + public async Task TestSubscribeQueryResponseAsyncEndAsync() { #region Arrange var serviceSubscription = new Mock(); + serviceSubscription.Setup(x => x.EndAsync()) + .Returns(ValueTask.CompletedTask); - var recievedActions = new List>>(); - var errorActions = new List>(); - var channels = new List(); - var groups = new List(); - var serviceMessages = new List(); + var serviceConnection = new Mock(); + + serviceConnection.Setup(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => + { + throw new NotImplementedException(); + }, (error) => { }); + await subscription.EndAsync(); + #endregion + + #region Assert + Assert.IsNotNull(subscription); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.EndAsync(), Times.Once); + #endregion + } - var serviceConnection = new Mock(); + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncAsyncCleanup() + { + #region Arrange + var serviceSubscription = new Mock(); + serviceSubscription.Setup(x => x.DisposeAsync()) + .Returns(ValueTask.CompletedTask); + + var serviceConnection = new Mock(); + + serviceConnection.Setup(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.As().Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => + { + throw new NotImplementedException(); + }, (error) => { }); + await subscription.DisposeAsync(); + #endregion + + #region Assert + Assert.IsNotNull(subscription); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.DisposeAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncWithNonAsyncCleanup() + { + #region Arrange + var serviceSubscription = new Mock(); + var serviceConnection = new Mock(); + + serviceConnection.Setup(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.As().Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => + { + throw new NotImplementedException(); + }, (error) => { }); + await subscription.DisposeAsync(); + #endregion + + #region Assert + Assert.IsNotNull(subscription); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.Dispose(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncSubscriptionsCleanup() + { + #region Arrange + var serviceSubscription = new Mock(); + + var serviceConnection = new Mock(); + + serviceConnection.Setup(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.As().Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => + { + throw new NotImplementedException(); + }, (error) => { }); + subscription.Dispose(); + #endregion + + #region Assert + Assert.IsNotNull(subscription); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.Dispose(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncWithThrowsConversionError() + { + #region Arrange + var message = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + + var serviceSubscription = new Mock(); + var globalConverter = new Mock(); + globalConverter.Setup(x => x.DecodeAsync(It.IsAny())) + .Returns(ValueTask.FromResult(null)); + globalConverter.Setup(x => x.EncodeAsync(It.IsAny())) + .Returns(ValueTask.FromResult([])); + globalConverter.Setup(x => x.DecodeAsync(It.IsAny())) + .Returns(ValueTask.FromResult(message)); + globalConverter.Setup(x => x.EncodeAsync(It.IsAny())) + .Returns(ValueTask.FromResult([])); + + var receivedActions = new List>>(); + + var serviceConnection = new Mock(); serviceConnection.Setup(x => x.SubscribeQueryAsync( - Capture.In>>(recievedActions), - Capture.In>(errorActions), - Capture.In(channels), - Capture.In(groups), - It.IsAny(), - It.IsAny())) + Capture.In>>(receivedActions), + It.IsAny>(), + It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (ServiceMessage message, TimeSpan timeout, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); - serviceMessages.Add(rmessage); - var result = await recievedActions[0](rmessage); + var rmessage = Helper.ProduceReceivedServiceMessage(message); + var result = await receivedActions[0](rmessage); return Helper.ProduceQueryResult(result); }); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object,defaultMessageEncoder:globalConverter.Object); - var message = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + var responseMessage = new BasicResponseMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); - var exception = new NullReferenceException("TestSubscribeQueryResponseWithNoExtendedAspects"); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeQueryResponseAsync((msg) => { + await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { messages.Add(msg); - return Task.FromResult(new QueryResponseMessage(responseMessage, null)); - }, (error) => exceptions.Add(error)); + return ValueTask.FromResult(new QueryResponseMessage(responseMessage, null)); + }, (error) => exceptions.Add(error), ignoreMessageHeader: true); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.QueryAsync(message); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); - - foreach (var act in errorActions) - act(exception); #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); - Assert.IsNotNull(subscription); Assert.IsNotNull(result); - Assert.AreEqual(1, recievedActions.Count); - Assert.AreEqual(1, channels.Count); - Assert.AreEqual(1, groups.Count); - Assert.AreEqual(1, serviceMessages.Count); - Assert.AreEqual(1, errorActions.Count); - Assert.AreEqual(1, exceptions.Count); - Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); - Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); - Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); - Assert.AreEqual(message, messages[0].Message); - Assert.AreEqual(exception, exceptions[0]); - Assert.IsFalse(result.IsError); - Assert.IsNull(result.Error); - Assert.AreEqual(result.Result, responseMessage); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); - Exception? disposeError = null; - try - { - subscription.Dispose(); - } - catch (Exception e) - { - disposeError=e; - } - Assert.IsNull(disposeError); + Assert.IsTrue(result.IsError); + Assert.IsFalse(string.IsNullOrWhiteSpace(result.Error)); + Assert.IsTrue(result.Error.Contains(typeof(BasicResponseMessage).FullName!)); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } diff --git a/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseWithoutQueryResponseTest.cs b/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseWithoutQueryResponseTest.cs new file mode 100644 index 0000000..982dcee --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/SubscribeQueryResponseWithoutQueryResponseTest.cs @@ -0,0 +1,91 @@ +using AutomatedTesting.Messages; +using Moq; +using MQContract.Attributes; +using MQContract.Interfaces.Service; +using MQContract.Interfaces; +using MQContract; +using System.Diagnostics; +using System.Reflection; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class SubscribeQueryResponseWithoutQueryResponseTest + { + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncWithNoExtendedAspects() + { + #region Arrange + var serviceSubscription = new Mock(); + var serviceSubObject = serviceSubscription.Object; + + var channels = new List(); + var groups = new List(); + List messages = []; + List> messageActions = []; + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(messageActions), It.IsAny>(), + Capture.In(channels), Capture.In(groups), It.IsAny())) + .ReturnsAsync(serviceSubObject); + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + messages.Add(message); + foreach (var action in messageActions) + action(new ReceivedServiceMessage(message.ID,message.MessageTypeID,message.Channel,message.Header,message.Data)); + return ValueTask.FromResult(new TransmissionResult(message.ID)); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + + var message = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + var responseMessage = new BasicResponseMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + #endregion + + #region Act + var receivedMessages = new List>(); + var exceptions = new List(); + var subscription = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { + receivedMessages.Add(msg); + return ValueTask.FromResult(new QueryResponseMessage(responseMessage, null)); + }, (error) => exceptions.Add(error)); + var stopwatch = Stopwatch.StartNew(); + var result = await contractConnection.QueryAsync(message); + stopwatch.Stop(); + System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); + + await subscription.EndAsync(); + #endregion + + #region Assert + Assert.IsTrue(await Helper.WaitForCount>(receivedMessages, 1, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount(messages, 2, TimeSpan.FromMinutes(1))); + Assert.IsNotNull(subscription); + Assert.IsNotNull(result); + Assert.AreEqual(2, channels.Count); + Assert.AreEqual(2, groups.Count); + Assert.AreEqual(2, messages.Count); + Assert.AreEqual(1, exceptions.Count); + Assert.AreEqual(typeof(BasicQueryMessage).GetCustomAttribute(false)?.Name, channels[0]); + Assert.IsNull(groups[0]); + Assert.IsNull(groups[1]); + Assert.AreEqual(receivedMessages[0].ID, messages[0].ID); + Assert.AreEqual(0,receivedMessages[0].Headers.Keys.Count()); + Assert.AreEqual(3, messages[0].Header.Keys.Count()); + Assert.AreEqual(message, receivedMessages[0].Message); + Assert.IsFalse(result.IsError); + Assert.IsNull(result.Error); + Assert.AreEqual(result.Result, responseMessage); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceSubscription.Verify(x => x.EndAsync(), Times.Exactly(2)); + #endregion + } + } +} diff --git a/AutomatedTesting/ContractConnectionTests/SubscribeTests.cs b/AutomatedTesting/ContractConnectionTests/SubscribeTests.cs index 85eafbc..8c7aeb5 100644 --- a/AutomatedTesting/ContractConnectionTests/SubscribeTests.cs +++ b/AutomatedTesting/ContractConnectionTests/SubscribeTests.cs @@ -20,37 +20,38 @@ public async Task TestSubscribeAsyncWithNoExtendedAspects() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new BasicMessage("TestSubscribeAsyncWithNoExtendedAspects"); var exception = new NullReferenceException("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { messages.Add(msg); - return Task.CompletedTask; + return ValueTask.CompletedTask; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.PublishAsync(message); @@ -62,7 +63,7 @@ public async Task TestSubscribeAsyncWithNoExtendedAspects() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result); Assert.AreEqual(1, actions.Count); @@ -72,19 +73,18 @@ public async Task TestSubscribeAsyncWithNoExtendedAspects() Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(BasicMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message, messages[0].Message); Assert.AreEqual(exception, exceptions[0]); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -98,16 +98,16 @@ public async Task TestSubscribeAsyncWithSpecificChannel() var channels = new List(); var channelName = "TestSubscribeAsyncWithSpecificChannel"; - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), Capture.In(channels), - It.IsAny(), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), Capture.In(channels), + It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var subscription1 = await contractConnection.SubscribeAsync((msg) => Task.CompletedTask, (error) => { },channel:channelName); - var subscription2 = await contractConnection.SubscribeAsync((msg) => Task.CompletedTask, (error) => { }, channel: channelName); + var subscription1 = await contractConnection.SubscribeAsync((msg) => ValueTask.CompletedTask, (error) => { }, channel: channelName); + var subscription2 = await contractConnection.SubscribeAsync((msg) => ValueTask.CompletedTask, (error) => { }, channel: channelName); #endregion #region Assert @@ -119,8 +119,7 @@ public async Task TestSubscribeAsyncWithSpecificChannel() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -134,16 +133,16 @@ public async Task TestSubscribeAsyncWithSpecificGroup() var groups = new List(); var groupName = "TestSubscribeAsyncWithSpecificGroup"; - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var subscription1 = await contractConnection.SubscribeAsync((msg) => Task.CompletedTask, (error) => { }, group:groupName); - var subscription2 = await contractConnection.SubscribeAsync((msg) => Task.CompletedTask, (error) => { }); + var subscription1 = await contractConnection.SubscribeAsync((msg) => ValueTask.CompletedTask, (error) => { }, group: groupName); + var subscription2 = await contractConnection.SubscribeAsync((msg) => ValueTask.CompletedTask, (error) => { }); #endregion #region Assert @@ -155,99 +154,95 @@ public async Task TestSubscribeAsyncWithSpecificGroup() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } [TestMethod] - public async Task TestSubscribeAsyncWithServiceChannelOptions() + public async Task TestSubscribeAsyncNoMessageChannelThrowsError() { #region Arrange var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var serviceChannelOptions = new TestServiceChannelOptions("TestSubscribeAsyncWithServiceChannelOptions"); - List options = []; - - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), - It.IsAny(), Capture.In(options), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var subscription = await contractConnection.SubscribeAsync((msg) => Task.CompletedTask, (error) => { }, options:serviceChannelOptions); + var exception = await Assert.ThrowsExceptionAsync(async () => await contractConnection.SubscribeAsync((msg) => ValueTask.CompletedTask, (error) => { })); #endregion #region Assert - Assert.IsNotNull(subscription); - Assert.AreEqual(1, options.Count); - Assert.IsInstanceOfType(options[0]); - Assert.AreEqual(serviceChannelOptions, options[0]); + Assert.IsNotNull(exception); + Assert.AreEqual("message must have a channel value (Parameter 'channel')", exception.Message); + Assert.AreEqual("channel", exception.ParamName); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); #endregion } [TestMethod] - public async Task TestSubscribeAsyncNoMessageChannelThrowsError() + public async Task TestSubscribeAsyncReturnFailedSubscription() { #region Arrange - var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((IServiceSubscription?)null); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var exception = await Assert.ThrowsExceptionAsync(() => contractConnection.SubscribeAsync((msg) => Task.CompletedTask, (error) => { })); + var exception = await Assert.ThrowsExceptionAsync(async ()=> await contractConnection.SubscribeAsync(msg => ValueTask.CompletedTask, err => { })); #endregion #region Assert Assert.IsNotNull(exception); - Assert.AreEqual("message must have a channel value (Parameter 'channel')", exception.Message); - Assert.AreEqual("channel", exception.ParamName); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Never); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); #endregion } + [TestMethod] - public async Task TestSubscribeAsyncReturnFailedSubscription() + public async Task TestSubscriptionsEndAsync() { #region Arrange + var serviceSubscription = new Mock(); + serviceSubscription.Setup(x => x.EndAsync()) + .Returns(ValueTask.CompletedTask); + var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((IServiceSubscription?)null); + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var exception = await Assert.ThrowsExceptionAsync(()=>contractConnection.SubscribeAsync(msg => Task.CompletedTask, err => { })); + var subscription = await contractConnection.SubscribeAsync(msg => ValueTask.CompletedTask, err => { }); + await subscription.EndAsync(); #endregion #region Assert - Assert.IsNotNull(exception); + Assert.IsNotNull(subscription); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.EndAsync(), Times.Once); #endregion } @@ -255,22 +250,22 @@ public async Task TestSubscribeAsyncReturnFailedSubscription() public async Task TestSubscribeAsyncCleanup() { #region Arrange - var serviceSubscription = new Mock(); - serviceSubscription.Setup(x => x.EndAsync()) - .Returns(Task.CompletedTask); + var serviceSubscription = new Mock(); + serviceSubscription.Setup(x => x.DisposeAsync()) + .Returns(ValueTask.CompletedTask); var serviceConnection = new Mock(); - serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.As().Object); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var subscription = await contractConnection.SubscribeAsync(msg => Task.CompletedTask, err => { }); - await subscription.EndAsync(); + var subscription = await contractConnection.SubscribeAsync(msg => ValueTask.CompletedTask, err => { }); + await subscription.DisposeAsync(); #endregion #region Assert @@ -278,9 +273,67 @@ public async Task TestSubscribeAsyncCleanup() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceSubscription.Verify(x => x.EndAsync(), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.DisposeAsync(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscribeAsyncWithNonAsyncCleanup() + { + #region Arrange + var serviceSubscription = new Mock(); + var serviceConnection = new Mock(); + + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.As().Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeAsync(msg => ValueTask.CompletedTask, err => { }); + await subscription.DisposeAsync(); + #endregion + + #region Assert + Assert.IsNotNull(subscription); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.Dispose(), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscriptionsCleanup() + { + #region Arrange + var serviceSubscription = new Mock(); + + var serviceConnection = new Mock(); + + serviceConnection.Setup(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.As().Object); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object); + #endregion + + #region Act + var subscription = await contractConnection.SubscribeAsync(msg => ValueTask.CompletedTask, err => { }); + subscription.Dispose(); + #endregion + + #region Assert + Assert.IsNotNull(subscription); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceSubscription.Verify(x => x.Dispose(), Times.Once); #endregion } @@ -292,39 +345,39 @@ public async Task TestSubscribeAsyncWithSynchronousActions() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message1 = new BasicMessage("TestSubscribeAsyncWithSynchronousActions1"); var message2 = new BasicMessage("TestSubscribeAsyncWithSynchronousActions2"); var exception = new NullReferenceException("TestSubscribeAsyncWithSynchronousActions"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { messages.Add(msg); - return Task.CompletedTask; - }, (error) => exceptions.Add(error),synchronous:true); + }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result1 = await contractConnection.PublishAsync(message1); stopwatch.Stop(); @@ -339,7 +392,7 @@ public async Task TestSubscribeAsyncWithSynchronousActions() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 2, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 2, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result1); Assert.IsNotNull(result2); @@ -350,20 +403,19 @@ public async Task TestSubscribeAsyncWithSynchronousActions() Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(BasicMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message1, messages[0].Message); Assert.AreEqual(message2, messages[1].Message); Assert.AreEqual(exception, exceptions[0]); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); #endregion } @@ -375,35 +427,36 @@ public async Task TestSubscribeAsyncWithErrorTriggeringInOurAction() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new BasicMessage("TestSubscribeAsyncWithNoExtendedAspects"); var exception = new NullReferenceException("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { throw exception; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); @@ -423,14 +476,13 @@ public async Task TestSubscribeAsyncWithErrorTriggeringInOurAction() Assert.AreEqual(0, messages.Count); Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(typeof(BasicMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(exception, exceptions[0]); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -442,34 +494,34 @@ public async Task TestSubscribeAsyncWithCorruptMetaDataHeaderException() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message,$"{message.MessageTypeID}:XXXX"); + var rmessage = Helper.ProduceReceivedServiceMessage(message,$"{message.MessageTypeID}:XXXX"); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new BasicMessage("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { return Task.CompletedTask; }, (error) => exceptions.Add(error)); + var subscription = await contractConnection.SubscribeAsync((msg) => { return ValueTask.CompletedTask; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.PublishAsync(message); stopwatch.Stop(); @@ -487,15 +539,14 @@ public async Task TestSubscribeAsyncWithCorruptMetaDataHeaderException() Assert.AreEqual(0, messages.Count); Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(typeof(BasicMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.IsInstanceOfType(exceptions[0]); Assert.AreEqual("MetaData is not valid", exceptions[0].Message); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -507,37 +558,38 @@ public async Task TestSubscribeAsyncWithDisposal() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new BasicMessage("TestSubscribeAsyncWithNoExtendedAspects"); var exception = new NullReferenceException("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { messages.Add(msg); - return Task.CompletedTask; + return ValueTask.CompletedTask; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.PublishAsync(message); @@ -549,7 +601,7 @@ public async Task TestSubscribeAsyncWithDisposal() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result); Assert.AreEqual(1, actions.Count); @@ -559,17 +611,17 @@ public async Task TestSubscribeAsyncWithDisposal() Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(BasicMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message, messages[0].Message); Assert.AreEqual(exception, exceptions[0]); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); Exception? disposeError = null; try { - subscription.Dispose(); + await subscription.EndAsync(); }catch(Exception e) { disposeError=e; @@ -578,9 +630,8 @@ public async Task TestSubscribeAsyncWithDisposal() #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -592,40 +643,41 @@ public async Task TestSubscribeAsyncWithSingleConversion() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new BasicMessage("TestSubscribeAsyncWithNoExtendedAspects"); var exception = new NullReferenceException("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { messages.Add(msg); - return Task.CompletedTask; + return ValueTask.CompletedTask; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); - var result = await contractConnection.PublishAsync(message,channel:typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name); + var result = await contractConnection.PublishAsync(message, channel: typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name); stopwatch.Stop(); System.Diagnostics.Trace.WriteLine($"Time to publish message {stopwatch.ElapsedMilliseconds}ms"); @@ -634,7 +686,7 @@ public async Task TestSubscribeAsyncWithSingleConversion() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result); Assert.AreEqual(1, actions.Count); @@ -644,19 +696,18 @@ public async Task TestSubscribeAsyncWithSingleConversion() Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message.Name, messages[0].Message.TestName); Assert.AreEqual(exception, exceptions[0]); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -668,37 +719,38 @@ public async Task TestSubscribeAsyncWithMultipleStepConversion() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new NoChannelMessage("TestSubscribeAsyncWithNoExtendedAspects"); var exception = new NullReferenceException("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { messages.Add(msg); - return Task.CompletedTask; + return ValueTask.CompletedTask; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.PublishAsync(message, channel: typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name); @@ -710,7 +762,7 @@ public async Task TestSubscribeAsyncWithMultipleStepConversion() #endregion #region Assert - Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); + Assert.IsTrue(await Helper.WaitForCount>(messages, 1, TimeSpan.FromMinutes(1))); Assert.IsNotNull(subscription); Assert.IsNotNull(result); Assert.AreEqual(1, actions.Count); @@ -720,19 +772,18 @@ public async Task TestSubscribeAsyncWithMultipleStepConversion() Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(1, exceptions.Count); Assert.AreEqual(typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(serviceMessages[0].ID, messages[0].ID); Assert.AreEqual(serviceMessages[0].Header.Keys.Count(), messages[0].Headers.Keys.Count()); - Assert.AreEqual(serviceMessages[0].RecievedTimestamp, messages[0].RecievedTimestamp); + Assert.AreEqual(serviceMessages[0].ReceivedTimestamp, messages[0].ReceivedTimestamp); Assert.AreEqual(message.TestName, messages[0].Message.TestName); Assert.AreEqual(exception, exceptions[0]); - System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].RecievedTimestamp).TotalMilliseconds}ms"); + System.Diagnostics.Trace.WriteLine($"Time to process message {messages[0].ProcessedTimestamp.Subtract(messages[0].ReceivedTimestamp).TotalMilliseconds}ms"); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } @@ -744,37 +795,38 @@ public async Task TestSubscribeAsyncWithNoConversionPath() var serviceSubscription = new Mock(); var serviceConnection = new Mock(); - var actions = new List>(); + var actions = new List>(); var errorActions = new List>(); var channels = new List(); var groups = new List(); - var serviceMessages = new List(); + var serviceMessages = new List(); - serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), - Capture.In(groups), It.IsAny(), It.IsAny())) + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), Capture.In>(errorActions), Capture.In(channels), + Capture.In(groups), It.IsAny())) .ReturnsAsync(serviceSubscription.Object); - serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((ServiceMessage message, IServiceChannelOptions options, CancellationToken cancellationToken) => + serviceConnection.Setup(x => x.PublishAsync(It.IsAny(), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => { - var rmessage = Helper.ProduceRecievedServiceMessage(message); + var rmessage = Helper.ProduceReceivedServiceMessage(message); serviceMessages.Add(rmessage); foreach (var act in actions) act(rmessage); - return Task.FromResult(transmissionResult); + return ValueTask.FromResult(transmissionResult); }); var message = new BasicQueryMessage("TestSubscribeAsyncWithNoExtendedAspects"); var exception = new NullReferenceException("TestSubscribeAsyncWithNoExtendedAspects"); - var contractConnection = new ContractConnection(serviceConnection.Object); + var contractConnection = ContractConnection.Instance(serviceConnection.Object); #endregion #region Act - var messages = new List>(); + var messages = new List>(); var exceptions = new List(); - var subscription = await contractConnection.SubscribeAsync((msg) => { + var subscription = await contractConnection.SubscribeAsync((msg) => + { messages.Add(msg); - return Task.CompletedTask; + return ValueTask.CompletedTask; }, (error) => exceptions.Add(error)); var stopwatch = Stopwatch.StartNew(); var result = await contractConnection.PublishAsync(message, channel: typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name); @@ -795,15 +847,14 @@ public async Task TestSubscribeAsyncWithNoConversionPath() Assert.AreEqual(0, messages.Count); Assert.AreEqual(1, errorActions.Count); Assert.AreEqual(typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.Name, channels[0]); - Assert.IsFalse(string.IsNullOrWhiteSpace(groups[0])); + Assert.IsNull(groups[0]); Assert.AreEqual(1, exceptions.OfType().Count()); Assert.IsTrue(exceptions.Contains(exception)); #endregion #region Verify - serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny()), Times.Once); - serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.SubscribeAsync(It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); #endregion } } diff --git a/AutomatedTesting/ContractConnectionTests/SystemMetricTests.cs b/AutomatedTesting/ContractConnectionTests/SystemMetricTests.cs new file mode 100644 index 0000000..401a14f --- /dev/null +++ b/AutomatedTesting/ContractConnectionTests/SystemMetricTests.cs @@ -0,0 +1,258 @@ +using AutomatedTesting.Messages; +using Moq; +using MQContract.Interfaces.Service; +using MQContract; +using MQContract.Attributes; +using System.Reflection; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using System.Diagnostics.Metrics; + +namespace AutomatedTesting.ContractConnectionTests +{ + [TestClass] + public class SystemMetricTests + { + private const string MeterName = "mqcontract"; + private static (MetricCollector sent,MetricCollector sentBytes,MetricCollector receivedCount,MetricCollector receivedBytes, + MetricCollector encodingDuration,MetricCollector decodingDuration) ProduceCollectors(Meter owningMeter,Type? messageType=null,string? channel = null) + { + var template = "messages"; + if (channel!=null) + template = $"channels.{channel}"; + else if (messageType!=null) + template = $"types.{messageType.GetCustomAttributes().Select(mn => mn.Value).FirstOrDefault(messageType.Name)}.{messageType.GetCustomAttributes().Select(mc => mc.Version.ToString()).FirstOrDefault("0.0.0.0").Replace('.', '_')}"; + return ( + new MetricCollector(owningMeter,$"{MeterName}.{template}.sent.count"), + new MetricCollector(owningMeter, $"{MeterName}.{template}.sent.bytes"), + new MetricCollector(owningMeter,$"{MeterName}.{template}.received.count"), + new MetricCollector(owningMeter, $"{MeterName}.{template}.received.bytes"), + new MetricCollector(owningMeter, $"{MeterName}.{template}.encodingduration"), + new MetricCollector(owningMeter, $"{MeterName}.{template}.decodingduration") + ); + } + + private static void CheckMeasurement(IReadOnlyList> readOnlyList, int count, long value) + { + Assert.AreEqual(count, readOnlyList.Count); + if (count>0) + Assert.AreEqual(value, readOnlyList[0].Value); + } + + private static void CheckMeasurement(IReadOnlyList> readOnlyList, int count, double value) + { + Assert.AreEqual(count, readOnlyList.Count); + if (count>0) + Assert.AreEqual(value, readOnlyList[0].Value); + } + + private static void CheckMeasurementGreaterThan(IReadOnlyList> readOnlyList, int count, long value) + { + Assert.AreEqual(count, readOnlyList.Count); + Assert.IsTrue(value> readOnlyList, int count, double value) + { + Assert.AreEqual(count, readOnlyList.Count); + Assert.IsTrue(value> left, IReadOnlyList> right) + =>Assert.IsTrue(left.Select(v=>v.Value).SequenceEqual(right.Select(v=>v.Value))); + + private static void AreMeasurementsEquals(IReadOnlyList> left, IReadOnlyList> right) + => Assert.IsTrue(left.Select(v => v.Value).SequenceEqual(right.Select(v => v.Value))); + + [TestMethod] + public void TestSystemMetricInitialization() + { + #region Arrange + var testMeter = new Meter("TestSystemMetricInitialization"); + var serviceConnection = new Mock(); + _ = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(testMeter, false); + #endregion + + #region Act + (MetricCollector sent, MetricCollector sentBytes, MetricCollector receivedCount, MetricCollector recievedBytes, + MetricCollector encodingDuration, MetricCollector decodingDuration) = ProduceCollectors(testMeter); + #endregion + + #region Assert + CheckMeasurement(sent.GetMeasurementSnapshot(), 0, 0); + CheckMeasurement(sentBytes.GetMeasurementSnapshot(), 0, 0); + CheckMeasurement(receivedCount.GetMeasurementSnapshot(), 0, 0); + CheckMeasurement(recievedBytes.GetMeasurementSnapshot(), 0, 0); + CheckMeasurement(encodingDuration.GetMeasurementSnapshot(), 0, 0); + CheckMeasurement(decodingDuration.GetMeasurementSnapshot(), 0, 0); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public async Task TestPublishAsyncSubscribeSystemMetrics() + { + #region Arrange + var testMeter = new Meter("TestPublishAsyncSubscribeSystemMetrics"); + var transmissionResult = new TransmissionResult(Guid.NewGuid().ToString()); + var serviceSubscription = new Mock(); + + var testMessage = new BasicMessage("testMessage"); + var channel = "TestSystemMetricsChannel"; + + List messages = []; + var actions = new List>(); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeAsync(Capture.In>(actions), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.PublishAsync(Capture.In(messages), It.IsAny())) + .Returns((ServiceMessage message, CancellationToken cancellationToken) => + { + var rmessage = Helper.ProduceReceivedServiceMessage(message); + foreach (var act in actions) + act(rmessage); + return ValueTask.FromResult(transmissionResult); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(testMeter,false); + #endregion + + #region Act + (MetricCollector sent, MetricCollector sentBytes, MetricCollector receivedCount, MetricCollector receivedBytes, + MetricCollector encodingDuration, MetricCollector decodingDuration) = ProduceCollectors(testMeter); + (MetricCollector sentType, MetricCollector sentBytesType, MetricCollector receivedCountType, MetricCollector receivedBytesType, + MetricCollector encodingDurationType, MetricCollector decodingDurationType) = ProduceCollectors(testMeter,messageType: typeof(BasicMessage)); + (MetricCollector sentChannel, MetricCollector sentBytesChannel, MetricCollector receivedCountChannel, MetricCollector receivedBytesChannel, + MetricCollector encodingDurationChannel, MetricCollector decodingDurationChannel) = ProduceCollectors(testMeter,channel: channel); + _ = await contractConnection.SubscribeAsync( + (msg) => ValueTask.CompletedTask, + (error) => { }, + channel:channel); + var result = await contractConnection.PublishAsync(testMessage,channel:channel); + _ = await Helper.WaitForCount(messages, 1, TimeSpan.FromMinutes(1)); + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(true); + #endregion + + #region Assert + Assert.IsNotNull(result); + Assert.AreEqual(transmissionResult, result); + CheckMeasurement(sent.GetMeasurementSnapshot(), 1, 1); + CheckMeasurementGreaterThan(sentBytes.GetMeasurementSnapshot(), 1, 0); + CheckMeasurement(receivedCount.GetMeasurementSnapshot(), 1, 1); + CheckMeasurementGreaterThan(receivedBytes.GetMeasurementSnapshot(), 1, 0); + CheckMeasurementGreaterThan(encodingDuration.GetMeasurementSnapshot(), 1, 0); + CheckMeasurementGreaterThan(decodingDuration.GetMeasurementSnapshot(), 1, 0); + + AreMeasurementsEquals(sent.GetMeasurementSnapshot(), sentType.GetMeasurementSnapshot()); + AreMeasurementsEquals(sentBytes.GetMeasurementSnapshot(), sentBytesType.GetMeasurementSnapshot()); + AreMeasurementsEquals(receivedCount.GetMeasurementSnapshot(), receivedCountType.GetMeasurementSnapshot()); + AreMeasurementsEquals(receivedBytes.GetMeasurementSnapshot(), receivedBytesType.GetMeasurementSnapshot()); + AreMeasurementsEquals(encodingDuration.GetMeasurementSnapshot(),encodingDurationType.GetMeasurementSnapshot()); + AreMeasurementsEquals(decodingDuration.GetMeasurementSnapshot(),decodingDurationType.GetMeasurementSnapshot()); + + AreMeasurementsEquals(sent.GetMeasurementSnapshot(), sentChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(sentBytes.GetMeasurementSnapshot(), sentBytesChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(receivedCount.GetMeasurementSnapshot(), receivedCountChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(receivedBytes.GetMeasurementSnapshot(), receivedBytesChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(encodingDuration.GetMeasurementSnapshot(), encodingDurationChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(decodingDuration.GetMeasurementSnapshot(), decodingDurationChannel.GetMeasurementSnapshot()); + #endregion + + #region Verify + serviceConnection.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + + [TestMethod] + public async Task TestSubscribeQueryResponseAsyncWithNoExtendedAspects() + { + #region Arrange + var testMeter = new Meter("TestSubscribeQueryResponseAsyncWithNoExtendedAspects"); + var serviceSubscription = new Mock(); + + var receivedActions = new List>>(); + + var serviceConnection = new Mock(); + serviceConnection.Setup(x => x.SubscribeQueryAsync( + Capture.In>>(receivedActions), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(serviceSubscription.Object); + serviceConnection.Setup(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) => + { + var rmessage = Helper.ProduceReceivedServiceMessage(message); + var result = await receivedActions[0](rmessage); + return Helper.ProduceQueryResult(result); + }); + + var contractConnection = ContractConnection.Instance(serviceConnection.Object) + .AddMetrics(testMeter, false); + + var message = new BasicQueryMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + var responseMessage = new BasicResponseMessage("TestSubscribeQueryResponseWithNoExtendedAspects"); + var channel = "TestQueryMetricChannel"; + #endregion + + #region Act + (MetricCollector sent, MetricCollector sentBytes, MetricCollector receivedCount, MetricCollector receivedBytes, + MetricCollector encodingDuration, MetricCollector decodingDuration) = ProduceCollectors(testMeter); + (MetricCollector sentRequestType, MetricCollector sentBytesRequestType, MetricCollector receivedCountRequestType, MetricCollector receivedBytesRequestType, + MetricCollector encodingDurationRequestType, MetricCollector decodingDurationRequestType) = ProduceCollectors(testMeter,messageType: typeof(BasicQueryMessage)); + (MetricCollector sentResponseType, MetricCollector sentBytesResponseType, MetricCollector receivedCountResponseType, MetricCollector receivedBytesResponseType, + MetricCollector encodingDurationResponseType, MetricCollector decodingDurationResponseType) = ProduceCollectors(testMeter,messageType: typeof(BasicResponseMessage)); + (MetricCollector sentChannel, MetricCollector sentBytesChannel, MetricCollector receivedCountChannel, MetricCollector receivedBytesChannel, + MetricCollector encodingDurationChannel, MetricCollector decodingDurationChannel) = ProduceCollectors(testMeter,channel: channel); + _ = await contractConnection.SubscribeQueryAsyncResponseAsync((msg) => + { + return ValueTask.FromResult(new QueryResponseMessage(responseMessage, null)); + }, (error) => { }, + channel:channel); + _ = await contractConnection.QueryAsync(message,channel:channel); + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(true); + #endregion + + #region Assert + CheckMeasurement(sent.GetMeasurementSnapshot(), 2, 1); + CheckMeasurementGreaterThan(sentBytes.GetMeasurementSnapshot(), 2, 0); + CheckMeasurement(receivedCount.GetMeasurementSnapshot(), 2, 1); + CheckMeasurementGreaterThan(receivedBytes.GetMeasurementSnapshot(), 2, 0); + CheckMeasurementGreaterThan(encodingDuration.GetMeasurementSnapshot(), 2, 0); + CheckMeasurementGreaterThan(decodingDuration.GetMeasurementSnapshot(), 2, 0); + + CheckMeasurement(sentRequestType.GetMeasurementSnapshot(), 1, 1); + CheckMeasurementGreaterThan(sentBytesRequestType.GetMeasurementSnapshot(), 1, 0); + CheckMeasurement(receivedCountRequestType.GetMeasurementSnapshot(), 1, 1); + CheckMeasurementGreaterThan(receivedBytesRequestType.GetMeasurementSnapshot(), 1, 0); + CheckMeasurementGreaterThan(encodingDurationRequestType.GetMeasurementSnapshot(), 1, 0); + CheckMeasurementGreaterThan(decodingDurationRequestType.GetMeasurementSnapshot(), 1, 0); + + CheckMeasurement(sentResponseType.GetMeasurementSnapshot(), 1, 1); + CheckMeasurementGreaterThan(sentBytesResponseType.GetMeasurementSnapshot(), 1, 0); + CheckMeasurement(receivedCountResponseType.GetMeasurementSnapshot(), 1, 1); + CheckMeasurementGreaterThan(receivedBytesResponseType.GetMeasurementSnapshot(), 1, 0); + CheckMeasurementGreaterThan(encodingDurationResponseType.GetMeasurementSnapshot(), 1, 0); + CheckMeasurementGreaterThan(decodingDurationResponseType.GetMeasurementSnapshot(), 1, 0); + + AreMeasurementsEquals(sentRequestType.GetMeasurementSnapshot(), sentChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(sentBytesRequestType.GetMeasurementSnapshot(), sentBytesChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(receivedCountRequestType.GetMeasurementSnapshot(), receivedCountChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(receivedBytesRequestType.GetMeasurementSnapshot(), receivedBytesChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(encodingDurationRequestType.GetMeasurementSnapshot(), encodingDurationChannel.GetMeasurementSnapshot()); + AreMeasurementsEquals(decodingDurationRequestType.GetMeasurementSnapshot(), decodingDurationChannel.GetMeasurementSnapshot()); + #endregion + + #region Verify + serviceConnection.Verify(x => x.SubscribeQueryAsync(It.IsAny>>(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + serviceConnection.Verify(x => x.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + #endregion + } + } +} diff --git a/AutomatedTesting/Converters/BasicMessageToNameAndVersionMessage.cs b/AutomatedTesting/Converters/BasicMessageToNameAndVersionMessage.cs index dff9977..a8691ef 100644 --- a/AutomatedTesting/Converters/BasicMessageToNameAndVersionMessage.cs +++ b/AutomatedTesting/Converters/BasicMessageToNameAndVersionMessage.cs @@ -5,7 +5,7 @@ namespace AutomatedTesting.Converters { internal class BasicMessageToNameAndVersionMessage : IMessageConverter { - public NamedAndVersionedMessage Convert(BasicMessage source) - => new(source.Name); + public ValueTask ConvertAsync(BasicMessage source) + => ValueTask.FromResult(new(source.Name)); } } diff --git a/AutomatedTesting/Converters/NoChannelMessageToBasicMessage.cs b/AutomatedTesting/Converters/NoChannelMessageToBasicMessage.cs index 82a2337..0be5d88 100644 --- a/AutomatedTesting/Converters/NoChannelMessageToBasicMessage.cs +++ b/AutomatedTesting/Converters/NoChannelMessageToBasicMessage.cs @@ -5,7 +5,7 @@ namespace AutomatedTesting.Converters { internal class NoChannelMessageToBasicMessage : IMessageConverter { - public BasicMessage Convert(NoChannelMessage source) - => new(source.TestName); + public ValueTask ConvertAsync(NoChannelMessage source) + => ValueTask.FromResult(new(source.TestName)); } } diff --git a/AutomatedTesting/Encoders/TestMessageEncoder.cs b/AutomatedTesting/Encoders/TestMessageEncoder.cs index 9b00a39..507f5a9 100644 --- a/AutomatedTesting/Encoders/TestMessageEncoder.cs +++ b/AutomatedTesting/Encoders/TestMessageEncoder.cs @@ -6,10 +6,10 @@ namespace AutomatedTesting.Encoders { internal class TestMessageEncoder : IMessageTypeEncoder { - public CustomEncoderMessage? Decode(Stream stream) - => new CustomEncoderMessage(Encoding.ASCII.GetString(new BinaryReader(stream).ReadBytes((int)stream.Length))); + public ValueTask DecodeAsync(Stream stream) + => ValueTask.FromResult(new CustomEncoderMessage(Encoding.ASCII.GetString(new BinaryReader(stream).ReadBytes((int)stream.Length)))); - public byte[] Encode(CustomEncoderMessage message) - => Encoding.ASCII.GetBytes(message.TestName); + public ValueTask EncodeAsync(CustomEncoderMessage message) + => ValueTask.FromResult(Encoding.ASCII.GetBytes(message.TestName)); } } diff --git a/AutomatedTesting/Encoders/TestMessageEncoderWithInjection.cs b/AutomatedTesting/Encoders/TestMessageEncoderWithInjection.cs index b0c4541..1326df1 100644 --- a/AutomatedTesting/Encoders/TestMessageEncoderWithInjection.cs +++ b/AutomatedTesting/Encoders/TestMessageEncoderWithInjection.cs @@ -8,14 +8,14 @@ namespace AutomatedTesting.Encoders internal class TestMessageEncoderWithInjection(IInjectableService injectableService) : IMessageTypeEncoder { - public CustomEncoderWithInjectionMessage? Decode(Stream stream) + public ValueTask DecodeAsync(Stream stream) { var message = Encoding.ASCII.GetString(new BinaryReader(stream).ReadBytes((int)stream.Length)); Assert.IsTrue(message.StartsWith($"{injectableService.Name}:")); - return new CustomEncoderWithInjectionMessage(message.Substring($"{injectableService.Name}:".Length)); + return ValueTask.FromResult(new CustomEncoderWithInjectionMessage(message.Substring($"{injectableService.Name}:".Length))); } - public byte[] Encode(CustomEncoderWithInjectionMessage message) - => Encoding.ASCII.GetBytes($"{injectableService.Name}:{message.TestName}"); + public ValueTask EncodeAsync(CustomEncoderWithInjectionMessage message) + => ValueTask.FromResult(Encoding.ASCII.GetBytes($"{injectableService.Name}:{message.TestName}")); } } diff --git a/AutomatedTesting/Encryptors/TestMessageEncryptor.cs b/AutomatedTesting/Encryptors/TestMessageEncryptor.cs index 7bfa2a1..c6f307b 100644 --- a/AutomatedTesting/Encryptors/TestMessageEncryptor.cs +++ b/AutomatedTesting/Encryptors/TestMessageEncryptor.cs @@ -8,21 +8,21 @@ internal class TestMessageEncryptor : IMessageTypeEncryptor DecryptAsync(Stream stream, MessageHeader headers) { Assert.IsNotNull(headers); Assert.IsTrue(headers.Keys.Contains(HeaderKey)); Assert.AreEqual(HeaderValue, headers[HeaderKey]); var data = new BinaryReader(stream).ReadBytes((int)stream.Length); - return new MemoryStream(data.Reverse().ToArray()); + return ValueTask.FromResult(new MemoryStream(data.Reverse().ToArray())); } - public byte[] Encrypt(byte[] data, out Dictionary headers) + public ValueTask EncryptAsync(byte[] data, out Dictionary headers) { headers = new([ new(HeaderKey,HeaderValue) ]); - return data.Reverse().ToArray(); + return ValueTask.FromResult(data.Reverse().ToArray()); } } } diff --git a/AutomatedTesting/Encryptors/TestMessageEncryptorWithInjection.cs b/AutomatedTesting/Encryptors/TestMessageEncryptorWithInjection.cs index 289f938..8150625 100644 --- a/AutomatedTesting/Encryptors/TestMessageEncryptorWithInjection.cs +++ b/AutomatedTesting/Encryptors/TestMessageEncryptorWithInjection.cs @@ -9,21 +9,21 @@ internal class TestMessageEncryptorWithInjection(IInjectableService injectableSe { private const string HeaderKey = "TestMessageEncryptorWithInjectionKey"; - public Stream Decrypt(Stream stream, MessageHeader headers) + public ValueTask DecryptAsync(Stream stream, MessageHeader headers) { Assert.IsNotNull(headers); Assert.IsTrue(headers.Keys.Contains(HeaderKey)); Assert.AreEqual(injectableService.Name, headers[HeaderKey]); var data = new BinaryReader(stream).ReadBytes((int)stream.Length); - return new MemoryStream(data.Reverse().ToArray()); + return ValueTask.FromResult(new MemoryStream(data.Reverse().ToArray())); } - public byte[] Encrypt(byte[] data, out Dictionary headers) + public ValueTask EncryptAsync(byte[] data, out Dictionary headers) { headers = new([ new(HeaderKey,injectableService.Name) ]); - return data.Reverse().ToArray(); + return ValueTask.FromResult(data.Reverse().ToArray()); } } } diff --git a/AutomatedTesting/Helper.cs b/AutomatedTesting/Helper.cs index 6a60c08..9cd1efa 100644 --- a/AutomatedTesting/Helper.cs +++ b/AutomatedTesting/Helper.cs @@ -12,7 +12,7 @@ public static IServiceProvider ProduceServiceProvider(string serviceName) return services.BuildServiceProvider(); } - public static RecievedServiceMessage ProduceRecievedServiceMessage(ServiceMessage message, string? messageTypeID = null) + public static ReceivedServiceMessage ProduceReceivedServiceMessage(ServiceMessage message, string? messageTypeID = null) => new(message.ID, messageTypeID??message.MessageTypeID, message.Channel, message.Header, message.Data); public static ServiceQueryResult ProduceQueryResult(ServiceMessage message) diff --git a/AutomatedTesting/Messages/BasicQueryMessage.cs b/AutomatedTesting/Messages/BasicQueryMessage.cs index 33e5a3c..e49cf6d 100644 --- a/AutomatedTesting/Messages/BasicQueryMessage.cs +++ b/AutomatedTesting/Messages/BasicQueryMessage.cs @@ -4,5 +4,6 @@ namespace AutomatedTesting.Messages { [MessageChannel("BasicQueryMessage")] [QueryResponseType(typeof(BasicResponseMessage))] + [QueryResponseChannel("BasicQueryResponse")] public record BasicQueryMessage(string TypeName) { } } diff --git a/AutomatedTesting/Messages/MessageHeaderTests.cs b/AutomatedTesting/Messages/MessageHeaderTests.cs new file mode 100644 index 0000000..584438f --- /dev/null +++ b/AutomatedTesting/Messages/MessageHeaderTests.cs @@ -0,0 +1,154 @@ +namespace AutomatedTesting.Messages +{ + [TestClass] + public class MessageHeaderTests + { + [TestMethod] + public void TestMessageHeaderPrimaryConstructor() + { + #region Arrange + IEnumerable> data = [ + new KeyValuePair("key1","value1"), + new KeyValuePair("key2","value2") + ]; + #endregion + + #region Act + var header = new MessageHeader(data); + #endregion + + #region Assert + Assert.AreEqual(2, header.Keys.Count()); + Assert.IsTrue(data.All(pair=>header.Keys.Contains(pair.Key) && Equals(header[pair.Key],pair.Value))); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public void TestMessageHeaderDictionaryConstructor() + { + #region Arrange + var data = new Dictionary([ + new KeyValuePair("key1","value1"), + new KeyValuePair("key2","value2") + ]); + #endregion + + #region Act + var header = new MessageHeader(data); + #endregion + + #region Assert + Assert.AreEqual(2, header.Keys.Count()); + Assert.IsTrue(data.All(pair => header.Keys.Contains(pair.Key) && Equals(header[pair.Key], pair.Value))); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public void TestMessageHeaderMergeConstructorWithOriginalAndExtension() + { + #region Arrange + var originalHeader = new MessageHeader([ + new KeyValuePair("key1","value1"), + new KeyValuePair("key2","value2") + ]); + var data = new Dictionary([ + new KeyValuePair("key3","value3"), + new KeyValuePair("key4","value4") + ]); + #endregion + + #region Act + var header = new MessageHeader(originalHeader,data); + #endregion + + #region Assert + Assert.AreEqual(4, header.Keys.Count()); + Assert.IsTrue(originalHeader.Keys.All(k => header.Keys.Contains(k) && Equals(header[k], originalHeader[k]))); + Assert.IsTrue(data.All(pair => header.Keys.Contains(pair.Key) && Equals(header[pair.Key], pair.Value))); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public void TestMessageHeaderMergeConstructorWithOriginalAndNullExtension() + { + #region Arrange + var originalHeader = new MessageHeader([ + new KeyValuePair("key1","value1"), + new KeyValuePair("key2","value2") + ]); + #endregion + + #region Act + var header = new MessageHeader(originalHeader, null); + #endregion + + #region Assert + Assert.AreEqual(2, header.Keys.Count()); + Assert.IsTrue(originalHeader.Keys.All(k => header.Keys.Contains(k) && Equals(header[k], originalHeader[k]))); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public void TestMessageHeaderMergeConstructorWithNullOriginalAndExtension() + { + #region Arrange + var data = new Dictionary([ + new KeyValuePair("key3","value3"), + new KeyValuePair("key4","value4") + ]); + #endregion + + #region Act + var header = new MessageHeader(null, data); + #endregion + + #region Assert + Assert.AreEqual(2, header.Keys.Count()); + Assert.IsTrue(data.All(pair => header.Keys.Contains(pair.Key) && Equals(header[pair.Key], pair.Value))); + #endregion + + #region Verify + #endregion + } + + [TestMethod] + public void TestMessageHeaderMergeConstructorWithOriginalAndExtensionWithSameKeys() + { + #region Arrange + var originalHeader = new MessageHeader([ + new KeyValuePair("key1","value1"), + new KeyValuePair("key2","value2") + ]); + var data = new Dictionary([ + new KeyValuePair("key1","value3"), + new KeyValuePair("key2","value4") + ]); + #endregion + + #region Act + var header = new MessageHeader(originalHeader, data); + #endregion + + #region Assert + Assert.AreEqual(2, header.Keys.Count()); + Assert.IsTrue(originalHeader.Keys.All(k => header.Keys.Contains(k) && !Equals(header[k], originalHeader[k]))); + Assert.IsTrue(data.All(pair => header.Keys.Contains(pair.Key) && Equals(header[pair.Key], pair.Value))); + #endregion + + #region Verify + #endregion + } + } +} diff --git a/AutomatedTesting/TestServiceChannelOptions.cs b/AutomatedTesting/TestServiceChannelOptions.cs deleted file mode 100644 index 9e92dfd..0000000 --- a/AutomatedTesting/TestServiceChannelOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MQContract.Interfaces.Service; - -namespace AutomatedTesting -{ - internal record TestServiceChannelOptions(string TestName) : IServiceChannelOptions { } -} diff --git a/Connectors/ActiveMQ/ActiveMQ.csproj b/Connectors/ActiveMQ/ActiveMQ.csproj new file mode 100644 index 0000000..1bab3fd --- /dev/null +++ b/Connectors/ActiveMQ/ActiveMQ.csproj @@ -0,0 +1,31 @@ + + + + + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) + ActiveMQ Connector for MQContract + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + True + \ + + + diff --git a/Connectors/ActiveMQ/Connection.cs b/Connectors/ActiveMQ/Connection.cs new file mode 100644 index 0000000..c887fbb --- /dev/null +++ b/Connectors/ActiveMQ/Connection.cs @@ -0,0 +1,122 @@ +using Apache.NMS; +using Apache.NMS.Util; +using MQContract.ActiveMQ.Subscriptions; +using MQContract.Interfaces.Service; +using MQContract.Messages; + +namespace MQContract.ActiveMQ +{ + /// + /// This is the MessageServiceConnection implemenation for using ActiveMQ + /// + public sealed class Connection : IMessageServiceConnection,IAsyncDisposable,IDisposable + { + private const string MESSAGE_TYPE_HEADER = "_MessageTypeID"; + private bool disposedValue; + + private readonly IConnection connection; + private readonly ISession session; + private readonly IMessageProducer producer; + + /// + /// Default constructor for creating instance + /// + /// The connection url to use + /// The username to use + /// The password to use + public Connection(Uri ConnectUri,string username,string password){ + var connectionFactory = new NMSConnectionFactory(ConnectUri); + connection = connectionFactory.CreateConnection(username,password); + connection.Start(); + session = connection.CreateSession(); + producer = session.CreateProducer(); + } + + uint? IMessageServiceConnection.MaxMessageBodySize => 4*1024*1024; + + private async ValueTask ProduceMessage(ServiceMessage message) + { + var msg = await session.CreateBytesMessageAsync(message.Data.ToArray()); + msg.NMSMessageId=message.ID; + msg.Properties[MESSAGE_TYPE_HEADER] = message.MessageTypeID; + foreach (var key in message.Header.Keys) + msg.Properties[key] = message.Header[key]; + return msg; + } + + private static MessageHeader ExtractHeaders(IPrimitiveMap properties, out string? messageTypeID) + { + var result = new Dictionary(); + messageTypeID = (string?)(properties.Contains(MESSAGE_TYPE_HEADER) ? properties[MESSAGE_TYPE_HEADER] : null); + foreach (var key in properties.Keys.OfType() + .Where(h =>!Equals(h, MESSAGE_TYPE_HEADER))) + result.Add(key, (string)properties[key]); + return new(result); + } + + internal static ReceivedServiceMessage ProduceMessage(string channel, IMessage message) + { + var headers = ExtractHeaders(message.Properties, out var messageTypeID); + return new( + message.NMSMessageId, + messageTypeID!, + channel, + headers, + message.Body(), + async ()=>await message.AcknowledgeAsync() + ); + } + + async ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) + { + try + { + await producer.SendAsync(SessionUtil.GetTopic(session, message.Channel), await ProduceMessage(message)); + return new TransmissionResult(message.ID); + } + catch (Exception ex) + { + return new TransmissionResult(message.ID, Error: ex.Message); + } + } + + async ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + { + var result = new SubscriptionBase((msg)=>messageReceived(ProduceMessage(channel,msg)), errorReceived,session, channel, group??Guid.NewGuid().ToString()); + await result.StartAsync(); + return result; + } + + async ValueTask IMessageServiceConnection.CloseAsync() + => await connection.StopAsync(); + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await connection.StopAsync().ConfigureAwait(true); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + connection.Stop(); + + producer.Dispose(); + session.Dispose(); + connection.Dispose(); + disposedValue=true; + } + } + + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Connectors/ActiveMQ/Exceptions.cs b/Connectors/ActiveMQ/Exceptions.cs new file mode 100644 index 0000000..ccbd183 --- /dev/null +++ b/Connectors/ActiveMQ/Exceptions.cs @@ -0,0 +1,18 @@ +namespace MQContract.ActiveMQ +{ + internal class QueryAsyncReponseException : Exception + { + internal QueryAsyncReponseException(string error) + : base(error) { } + } + internal class QueryExecutionFailedException : Exception + { + internal QueryExecutionFailedException() + : base("Failed to execute query") { } + } + internal class QueryResultMissingException : Exception + { + internal QueryResultMissingException() + : base("Query result not found") { } + } +} diff --git a/Connectors/ActiveMQ/Readme.md b/Connectors/ActiveMQ/Readme.md new file mode 100644 index 0000000..3107ac1 --- /dev/null +++ b/Connectors/ActiveMQ/Readme.md @@ -0,0 +1,33 @@ + +# MQContract.ActiveMQ + +## Contents + +- [Connection](#T-MQContract-ActiveMQ-Connection 'MQContract.ActiveMQ.Connection') + - [#ctor(ConnectUri,username,password)](#M-MQContract-ActiveMQ-Connection-#ctor-System-Uri,System-String,System-String- 'MQContract.ActiveMQ.Connection.#ctor(System.Uri,System.String,System.String)') + + +## Connection `type` + +##### Namespace + +MQContract.ActiveMQ + +##### Summary + +This is the MessageServiceConnection implemenation for using ActiveMQ + + +### #ctor(ConnectUri,username,password) `constructor` + +##### Summary + +Default constructor for creating instance + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| ConnectUri | [System.Uri](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Uri 'System.Uri') | The connection url to use | +| username | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The username to use | +| password | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The password to use | diff --git a/Connectors/ActiveMQ/Subscriptions/SubscriptionBase.cs b/Connectors/ActiveMQ/Subscriptions/SubscriptionBase.cs new file mode 100644 index 0000000..b9b5d33 --- /dev/null +++ b/Connectors/ActiveMQ/Subscriptions/SubscriptionBase.cs @@ -0,0 +1,53 @@ +using Apache.NMS; +using Apache.NMS.Util; +using MQContract.Interfaces.Service; + +namespace MQContract.ActiveMQ.Subscriptions +{ + internal class SubscriptionBase(Action messageReceived,Action errorReceived,ISession session, string channel,string group) : IServiceSubscription + { + private bool disposedValue; + private IMessageConsumer? consumer; + protected readonly CancellationTokenSource cancelToken = new(); + protected string Channel => channel; + + internal async ValueTask StartAsync() + { + consumer = await session.CreateSharedConsumerAsync(SessionUtil.GetTopic(session, channel),group); + _=Task.Run(async () => + { + while (!cancelToken.IsCancellationRequested) + { + try + { + var msg = await consumer.ReceiveAsync(); + if (msg!=null) + messageReceived(msg); + } + catch (Exception ex) + { + errorReceived(ex); + } + } + }); + } + + public async ValueTask EndAsync() + { + if (!cancelToken.IsCancellationRequested) + await cancelToken.CancelAsync(); + if (consumer!=null) + await consumer.CloseAsync(); + } + + public async ValueTask DisposeAsync() + { + if (!disposedValue) + { + disposedValue=true; + await EndAsync(); + consumer?.Dispose(); + } + } + } +} diff --git a/Connectors/HiveMQ/Connection.cs b/Connectors/HiveMQ/Connection.cs new file mode 100644 index 0000000..df06fbd --- /dev/null +++ b/Connectors/HiveMQ/Connection.cs @@ -0,0 +1,192 @@ +using HiveMQtt.Client; +using HiveMQtt.Client.Options; +using HiveMQtt.MQTT5.Types; +using MQContract.Interfaces.Service; +using MQContract.Messages; + +namespace MQContract.HiveMQ +{ + /// + /// This is the MessageServiceConnection implementation for using HiveMQ + /// + public class Connection : IInboxQueryableMessageServiceConnection, IDisposable + { + private readonly HiveMQClientOptions clientOptions; + private readonly HiveMQClient client; + private readonly Guid connectionID = Guid.NewGuid(); + private bool disposedValue; + + /// + /// Default constructor that requires the HiveMQ client options settings to be provided + /// + /// The required client options to connect to the HiveMQ instance + public Connection(HiveMQClientOptions clientOptions) + { + this.clientOptions = clientOptions; + client = new(clientOptions); + var connectTask = client.ConnectAsync(); + connectTask.Wait(); + if (connectTask.Result.ReasonCode!=HiveMQtt.MQTT5.ReasonCodes.ConnAckReasonCode.Success) + throw new ConnectionFailedException(connectTask.Result.ReasonString); + } + + uint? IMessageServiceConnection.MaxMessageBodySize => (uint?)clientOptions.ClientMaximumPacketSize; + + /// + /// The default timeout to allow for a Query Response call to execute, defaults to 1 minute + /// + public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromMinutes(1); + + async ValueTask IMessageServiceConnection.CloseAsync() + => await client.DisconnectAsync(); + + private const string MessageID = "_ID"; + private const string MessageTypeID = "_MessageTypeID"; + private const string ResponseID = "_MessageResponseID"; + + private static MQTT5PublishMessage ConvertMessage(ServiceMessage message,string? responseTopic=null,Guid? responseID=null,string? respondToTopic=null) + => new() + { + Topic=respondToTopic??message.Channel, + QoS=QualityOfService.AtLeastOnceDelivery, + Payload=message.Data.ToArray(), + ResponseTopic=responseTopic, + UserProperties=new Dictionary( + message.Header.Keys + .Select(k=>new KeyValuePair(k,message.Header[k]!)) + .Concat([ + new(MessageID,message.ID), + new(MessageTypeID,message.MessageTypeID) + ]) + .Concat(responseID!=null ?[new(ResponseID,responseID.Value.ToString())] : []) + ) + }; + + private static ReceivedServiceMessage ConvertMessage(MQTT5PublishMessage message, out string? responseID) + { + message.UserProperties.TryGetValue(ResponseID, out responseID); + return new( + message.UserProperties[MessageID], + message.UserProperties[MessageTypeID], + message.Topic!, + new(message.UserProperties.AsEnumerable().Where(pair => !Equals(pair.Key, MessageID)&&!Equals(pair.Key, MessageTypeID)&&!Equals(pair.Key,ResponseID))), + message.Payload + ); + } + + async ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) + { + try + { + _ = await client.PublishAsync(ConvertMessage(message), cancellationToken); + }catch(Exception e) + { + return new(message.ID, e.Message); + } + return new(message.ID); + } + + async ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + { + var result = new Subscription( + clientOptions, + (msg) => + { + try + { + messageReceived(ConvertMessage(msg,out _)); + }catch(Exception e) + { + errorReceived(e); + } + } + ,channel,group); + await result.EstablishAsync(); + return result; + } + + private string InboxChannel => $"_inbox/{connectionID}"; + + async ValueTask IInboxQueryableMessageServiceConnection.EstablishInboxSubscriptionAsync(Action messageReceived, CancellationToken cancellationToken) + { + var result = new Subscription( + clientOptions, + (msg) => + { + var incomingMessage = ConvertMessage(msg, out var responseID); + if (responseID!=null && Guid.TryParse(responseID, out var responseGuid)) + { + messageReceived(new( + incomingMessage.ID, + incomingMessage.MessageTypeID, + InboxChannel, + incomingMessage.Header, + responseGuid, + incomingMessage.Data, + incomingMessage.Acknowledge + )); + } + }, + InboxChannel, + null + ); + await result.EstablishAsync(); + return result; + } + + async ValueTask IInboxQueryableMessageServiceConnection.QueryAsync(ServiceMessage message, Guid correlationID, CancellationToken cancellationToken) + { + try + { + _ = await client.PublishAsync(ConvertMessage(message,responseTopic:InboxChannel,responseID:correlationID), cancellationToken); + } + catch (Exception e) + { + return new(message.ID, e.Message); + } + return new(message.ID); + } + + async ValueTask IQueryableMessageServiceConnection.SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + { + var result = new Subscription( + clientOptions, + async (msg) => + { + try + { + var result = await messageReceived(ConvertMessage(msg, out var responseID)); + _ = await client.PublishAsync(ConvertMessage(result, responseID: new Guid(responseID!), respondToTopic: msg.ResponseTopic), cancellationToken); + } + catch (Exception e) + { + errorReceived(e); + } + }, + channel, + group + ); + await result.EstablishAsync(); + return result; + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + client.Dispose(); + } + disposedValue=true; + } + } + + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Connectors/HiveMQ/Exceptions.cs b/Connectors/HiveMQ/Exceptions.cs new file mode 100644 index 0000000..fd00116 --- /dev/null +++ b/Connectors/HiveMQ/Exceptions.cs @@ -0,0 +1,12 @@ +namespace MQContract.HiveMQ +{ + /// + /// Thrown when the service connection is unable to connect to the HiveMQTT server + /// + public class ConnectionFailedException : Exception + { + internal ConnectionFailedException(string? reason) + : base($"Failed to connect: {reason}") + { } + } +} diff --git a/Connectors/HiveMQ/HiveMQ.csproj b/Connectors/HiveMQ/HiveMQ.csproj new file mode 100644 index 0000000..eaaabcf --- /dev/null +++ b/Connectors/HiveMQ/HiveMQ.csproj @@ -0,0 +1,34 @@ + + + + + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) + HiveMQ Connector for MQContract + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + True + \ + + + diff --git a/Connectors/HiveMQ/Readme.md b/Connectors/HiveMQ/Readme.md new file mode 100644 index 0000000..3b5b266 --- /dev/null +++ b/Connectors/HiveMQ/Readme.md @@ -0,0 +1,51 @@ + +# MQContract.HiveMQ + +## Contents + +- [Connection](#T-MQContract-HiveMQ-Connection 'MQContract.HiveMQ.Connection') + - [#ctor(clientOptions)](#M-MQContract-HiveMQ-Connection-#ctor-HiveMQtt-Client-Options-HiveMQClientOptions- 'MQContract.HiveMQ.Connection.#ctor(HiveMQtt.Client.Options.HiveMQClientOptions)') + - [DefaultTimeout](#P-MQContract-HiveMQ-Connection-DefaultTimeout 'MQContract.HiveMQ.Connection.DefaultTimeout') +- [ConnectionFailedException](#T-MQContract-HiveMQ-ConnectionFailedException 'MQContract.HiveMQ.ConnectionFailedException') + + +## Connection `type` + +##### Namespace + +MQContract.HiveMQ + +##### Summary + +This is the MessageServiceConnection implementation for using HiveMQ + + +### #ctor(clientOptions) `constructor` + +##### Summary + +Default constructor that requires the HiveMQ client options settings to be provided + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| clientOptions | [HiveMQtt.Client.Options.HiveMQClientOptions](#T-HiveMQtt-Client-Options-HiveMQClientOptions 'HiveMQtt.Client.Options.HiveMQClientOptions') | The required client options to connect to the HiveMQ instance | + + +### DefaultTimeout `property` + +##### Summary + +The default timeout to allow for a Query Response call to execute, defaults to 1 minute + + +## ConnectionFailedException `type` + +##### Namespace + +MQContract.HiveMQ + +##### Summary + +Thrown when the service connection is unable to connect to the HiveMQTT server diff --git a/Connectors/HiveMQ/Subscription.cs b/Connectors/HiveMQ/Subscription.cs new file mode 100644 index 0000000..e10161b --- /dev/null +++ b/Connectors/HiveMQ/Subscription.cs @@ -0,0 +1,60 @@ +using HiveMQtt.Client; +using HiveMQtt.Client.Options; +using HiveMQtt.MQTT5.Types; +using MQContract.Interfaces.Service; + +namespace MQContract.HiveMQ +{ + internal class Subscription(HiveMQClientOptions clientOptions, Action messageReceived,string channel, string? group) : IServiceSubscription,IDisposable + { + private readonly HiveMQClient client = new(CloneOptions(clientOptions,channel)); + + private static HiveMQClientOptions CloneOptions(HiveMQClientOptions clientOptions,string channel) + { + var result = new HiveMQClientOptions(); + foreach (var prop in typeof(HiveMQClientOptions).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance).Where(p => !Equals(p.Name, nameof(clientOptions.ClientId)))) + prop.SetValue(result, prop.GetValue(clientOptions, [])); + result.ClientId=$"{clientOptions.ClientId}.{channel}.{Guid.NewGuid()}"; + return result; + } + + private bool disposedValue; + + private string Topic => $"{(group==null ? "" : $"$share/{group}/")}{channel}"; + + public async ValueTask EstablishAsync() + { + client.OnMessageReceived += (sender, args) + => messageReceived(args.PublishMessage); + var connectResult = await client.ConnectAsync(); + if (connectResult.ReasonCode != HiveMQtt.MQTT5.ReasonCodes.ConnAckReasonCode.Success) + throw new Exception($"Failed to connect: {connectResult.ReasonString}"); + _ = await client.SubscribeAsync(Topic, HiveMQtt.MQTT5.Types.QualityOfService.AtLeastOnceDelivery); + } + + async ValueTask IServiceSubscription.EndAsync() + { + await client.UnsubscribeAsync(Topic); + await client.DisconnectAsync(); + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + client.Dispose(); + } + disposedValue=true; + } + } + + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Connectors/InMemory/Connection.cs b/Connectors/InMemory/Connection.cs new file mode 100644 index 0000000..b9982c1 --- /dev/null +++ b/Connectors/InMemory/Connection.cs @@ -0,0 +1,62 @@ +using MQContract.Interfaces.Service; +using MQContract.Messages; +using System.Collections.Concurrent; + +namespace MQContract.InMemory +{ + /// + /// Used as an in memory connection messaging system where all transmission are done through Channels within the connection. You must use the same underlying connection. + /// + public class Connection : IInboxQueryableMessageServiceConnection + { + private readonly ConcurrentDictionary channels = []; + private readonly string inboxChannel = $"_inbox/{Guid.NewGuid()}"; + /// + /// Default timeout for a given QueryResponse call + /// default: 1 minute + /// + public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromMinutes(1); + + /// + /// Maximum allowed message body size in bytes + /// default: 4MB + /// + public uint? MaxMessageBodySize { get; init; } = 1024*1024*4; + + private MessageChannel GetChannel(string channel) + { + if (!channels.TryGetValue(channel, out MessageChannel? messageChannel)) + { + messageChannel = new MessageChannel(); + channels.TryAdd(channel, messageChannel); + } + return messageChannel; + } + + ValueTask IMessageServiceConnection.CloseAsync() + { + var keys = channels.Keys.ToArray(); + foreach(var key in keys) + { + if (channels.TryRemove(key, out var channel)) + channel.Close(); + } + return ValueTask.CompletedTask; + } + + ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) + => GetChannel(message.Channel).PublishAsync(message, cancellationToken); + + ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + => GetChannel(channel).RegisterSubscriptionAsync(messageReceived, errorReceived, group, cancellationToken); + + ValueTask IQueryableMessageServiceConnection.SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + => GetChannel(channel).RegisterQuerySubscriptionAsync(messageReceived, errorReceived, + async (response) =>await GetChannel(inboxChannel).PublishAsync(response,cancellationToken), group, cancellationToken); + + ValueTask IInboxQueryableMessageServiceConnection.EstablishInboxSubscriptionAsync(Action messageReceived, CancellationToken cancellationToken) + => GetChannel(inboxChannel).EstablishInboxSubscriptionAsync(messageReceived, cancellationToken); + ValueTask IInboxQueryableMessageServiceConnection.QueryAsync(ServiceMessage message, Guid correlationID, CancellationToken cancellationToken) + => GetChannel(message.Channel).QueryAsync(message,inboxChannel, correlationID, cancellationToken); + } +} diff --git a/Connectors/InMemory/InMemory.csproj b/Connectors/InMemory/InMemory.csproj new file mode 100644 index 0000000..a7c7a98 --- /dev/null +++ b/Connectors/InMemory/InMemory.csproj @@ -0,0 +1,32 @@ + + + + + + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) + In Memory Connector for MQContract + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + True + \ + + + + diff --git a/Connectors/InMemory/InternalServiceMessage.cs b/Connectors/InMemory/InternalServiceMessage.cs new file mode 100644 index 0000000..df31052 --- /dev/null +++ b/Connectors/InMemory/InternalServiceMessage.cs @@ -0,0 +1,8 @@ +using MQContract.Messages; + +namespace MQContract.InMemory +{ + internal record InternalServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, ReadOnlyMemory Data,Guid? CorrelationID=null,string? ReplyChannel=null) + : ServiceMessage(ID,MessageTypeID,Channel,Header, Data) + { } +} diff --git a/Connectors/InMemory/MessageChannel.cs b/Connectors/InMemory/MessageChannel.cs new file mode 100644 index 0000000..f3d4d58 --- /dev/null +++ b/Connectors/InMemory/MessageChannel.cs @@ -0,0 +1,119 @@ +using MQContract.Interfaces.Service; +using MQContract.Messages; + +namespace MQContract.InMemory +{ + internal class MessageChannel + { + private readonly SemaphoreSlim semLock = new(1, 1); + private readonly List groups = []; + + private async ValueTask Publish(InternalServiceMessage message, CancellationToken cancellationToken) + { + await semLock.WaitAsync(cancellationToken); + var tasks = groups.Select(grp => grp.PublishMessage(message).AsTask()).ToArray(); + semLock.Release(); + await Task.WhenAll(tasks); + return Array.TrueForAll(tasks, t => t.Result); + } + + public void Close() + { + semLock.Wait(); + foreach (var group in groups.ToArray()) + group.Close(); + groups.Clear(); + semLock.Release(); + } + + internal async ValueTask PublishAsync(ServiceMessage message, CancellationToken cancellationToken) + { + if (!await Publish(new(message.ID,message.MessageTypeID,message.Channel,message.Header,message.Data), cancellationToken)) + return new(message.ID, "Unable to trasmit"); + return new(message.ID); + } + + internal async ValueTask PublishAsync(InternalServiceMessage message, CancellationToken cancellationToken) + => await Publish(message, cancellationToken); + + internal async ValueTask QueryAsync(ServiceMessage message,string inbox, Guid correlationID, CancellationToken cancellationToken) + { + if (!await Publish(new(message.ID,message.MessageTypeID,message.Channel,message.Header,message.Data,correlationID,inbox), cancellationToken)) + return new(message.ID, "Unable to trasmit"); + return new(message.ID); + } + + private async ValueTask GetGroupAsync(string? group) + { + group??=Guid.NewGuid().ToString(); + await semLock.WaitAsync(); + var grp = groups.Find(g => Equals(g.Group, group)); + if (grp==null) + { + grp = new MessageGroup(group, g => + { + semLock.Wait(); + groups.Remove(g); + semLock.Release(); + }); + groups.Add(grp); + } + semLock.Release(); + return grp; + } + + private async ValueTask CreateSubscription(Func processMessage,Action errorReceived,string? group,CancellationToken cancellationToken) + { + var sub = new Subscription(await GetGroupAsync(group), async (recievedMessage) => + { + try + { + await processMessage(recievedMessage); + } + catch (Exception ex) + { + errorReceived(ex); + } + }); + sub.Start(); + return sub; + } + + internal async ValueTask RegisterQuerySubscriptionAsync(Func> messageReceived, Action errorReceived, Action publishResponse, string? group, CancellationToken cancellationToken) + => await CreateSubscription( + async (recievedMessage) => + { + var result = await messageReceived(new(recievedMessage.ID, recievedMessage.MessageTypeID, recievedMessage.Channel, recievedMessage.Header, recievedMessage.Data)); + publishResponse(new(result.ID, result.MessageTypeID, recievedMessage.ReplyChannel!, result.Header, result.Data, recievedMessage.CorrelationID)); + }, + errorReceived, + group, + cancellationToken + ); + + internal async ValueTask RegisterSubscriptionAsync(Action messageReceived, Action errorReceived, string? group, CancellationToken cancellationToken) + => await CreateSubscription( + (receivedMessage) => + { + messageReceived(new(receivedMessage.ID, receivedMessage.MessageTypeID, receivedMessage.Channel, receivedMessage.Header, receivedMessage.Data)); + return ValueTask.CompletedTask; + }, + errorReceived, + group, + cancellationToken + ); + + internal async ValueTask EstablishInboxSubscriptionAsync(Action messageReceived, CancellationToken cancellationToken) + => await CreateSubscription( + (receivedMessage) => + { + if (receivedMessage.CorrelationID!=null) + messageReceived(new(receivedMessage.ID, receivedMessage.MessageTypeID, receivedMessage.Channel, receivedMessage.Header, receivedMessage.CorrelationID.Value, receivedMessage.Data)); + return ValueTask.CompletedTask; + }, + (error) => { }, + null, + cancellationToken + ); + } +} diff --git a/Connectors/InMemory/MessageGroup.cs b/Connectors/InMemory/MessageGroup.cs new file mode 100644 index 0000000..95b2f43 --- /dev/null +++ b/Connectors/InMemory/MessageGroup.cs @@ -0,0 +1,53 @@ +using System.Threading.Channels; + +namespace MQContract.InMemory +{ + internal class MessageGroup(string group,Action removeMe) + { + private readonly SemaphoreSlim semLock = new(1, 1); + private readonly List> channels = []; + private int index = 0; + public string Group => group; + + public Channel Register() + { + var result = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader=true,SingleWriter=true}); + channels.Add(result); + return result; + } + + public async ValueTask UnregisterAsync(Channel channel) + { + await semLock.WaitAsync(); + channels.Remove(channel); + if (channels.Count == 0) + removeMe(this); + semLock.Release(); + } + + public async ValueTask PublishMessage(InternalServiceMessage message) + { + var success = false; + await semLock.WaitAsync(); + if (index>=channels.Count) + index=0; + if (index +# MQContract.InMemory + +## Contents + +- [Connection](#T-MQContract-InMemory-Connection 'MQContract.InMemory.Connection') + - [DefaultTimeout](#P-MQContract-InMemory-Connection-DefaultTimeout 'MQContract.InMemory.Connection.DefaultTimeout') + - [MaxMessageBodySize](#P-MQContract-InMemory-Connection-MaxMessageBodySize 'MQContract.InMemory.Connection.MaxMessageBodySize') + + +## Connection `type` + +##### Namespace + +MQContract.InMemory + +##### Summary + +Used as an in memory connection messaging system where all transmission are done through Channels within the connection. You must use the same underlying connection. + + +### DefaultTimeout `property` + +##### Summary + +Default timeout for a given QueryResponse call +default: 1 minute + + +### MaxMessageBodySize `property` + +##### Summary + +Maximum allowed message body size in bytes +default: 4MB diff --git a/Connectors/InMemory/Subscription.cs b/Connectors/InMemory/Subscription.cs new file mode 100644 index 0000000..c168019 --- /dev/null +++ b/Connectors/InMemory/Subscription.cs @@ -0,0 +1,28 @@ +using MQContract.Interfaces.Service; +using System.Threading.Channels; + +namespace MQContract.InMemory +{ + internal class Subscription(MessageGroup group, Func messageRecieved) : IServiceSubscription + { + private readonly Channel channel = group.Register(); + + public void Start() + { + Task.Run(async () => + { + while (await channel.Reader.WaitToReadAsync()) + { + var message = await channel.Reader.ReadAsync(); + await messageRecieved(message); + } + }); + } + + async ValueTask IServiceSubscription.EndAsync() + { + channel.Writer.TryComplete(); + await group.UnregisterAsync(channel); + } + } +} diff --git a/Connectors/Kafka/Connection.cs b/Connectors/Kafka/Connection.cs index 0cb4ed7..c379c4f 100644 --- a/Connectors/Kafka/Connection.cs +++ b/Connectors/Kafka/Connection.cs @@ -1,9 +1,7 @@ using Confluent.Kafka; using MQContract.Interfaces.Service; -using MQContract.Kafka.Options; using MQContract.Kafka.Subscriptions; using MQContract.Messages; -using MQContract.NATS.Subscriptions; using System.Text; namespace MQContract.Kafka @@ -11,32 +9,15 @@ namespace MQContract.Kafka /// /// This is the MessageServiceConnection implementation for using Kafka /// - /// - public class Connection(ClientConfig clientConfig) : IMessageServiceConnection + /// The Kafka Client Configuration to provide + public sealed class Connection(ClientConfig clientConfig) : IMessageServiceConnection { private const string MESSAGE_TYPE_HEADER = "_MessageTypeID"; - private const string QUERY_IDENTIFIER_HEADER = "_QueryClientID"; - private const string REPLY_ID = "_QueryReplyID"; - private const string REPLY_CHANNEL_HEADER = "_QueryReplyChannel"; - private const string ERROR_MESSAGE_TYPE_ID = "KafkaQueryError"; private readonly IProducer producer = new ProducerBuilder(clientConfig).Build(); private readonly ClientConfig clientConfig = clientConfig; - private readonly List subscriptions = []; - private readonly SemaphoreSlim dataLock = new(1, 1); - private readonly Guid Identifier = Guid.NewGuid(); - private bool disposedValue; - /// - /// The maximum message body size allowed - /// - public int? MaxMessageBodySize => clientConfig.MessageMaxBytes; - - /// - /// The default timeout to use for RPC calls when not specified by the class or in the call. - /// DEFAULT:1 minute if not specified inside the connection options - /// - public TimeSpan DefaultTimout { get; init; } = TimeSpan.FromMinutes(1); + uint? IMessageServiceConnection.MaxMessageBodySize => (uint)Math.Abs(clientConfig.MessageMaxBytes??(1024*1024)); internal static byte[] EncodeHeaderValue(string value) => UTF8Encoding.UTF8.GetBytes(value); @@ -44,30 +25,19 @@ internal static byte[] EncodeHeaderValue(string value) internal static string DecodeHeaderValue(byte[] value) => UTF8Encoding.UTF8.GetString(value); - internal static Headers ExtractHeaders(ServiceMessage message, Guid? queryClientID = null, Guid? replyID = null,string? replyChannel=null) + internal static Headers ExtractHeaders(ServiceMessage message) { var result = new Headers(); foreach (var key in message.Header.Keys) result.Add(key, EncodeHeaderValue(message.Header[key]!)); result.Add(MESSAGE_TYPE_HEADER, EncodeHeaderValue(message.MessageTypeID)); - if (queryClientID!=null) - result.Add(QUERY_IDENTIFIER_HEADER, queryClientID.Value.ToByteArray()); - if (replyID!=null) - result.Add(REPLY_ID,replyID.Value.ToByteArray()); - if (replyChannel!=null) - result.Add(REPLY_CHANNEL_HEADER,EncodeHeaderValue(replyChannel!)); return result; } private static MessageHeader ExtractHeaders(Headers header) - => new MessageHeader( + => new( header - .Where(h => - !Equals(h.Key, REPLY_ID) - &&!Equals(h.Key, REPLY_CHANNEL_HEADER) - &&!Equals(h.Key, MESSAGE_TYPE_HEADER) - &&!Equals(h.Key,QUERY_IDENTIFIER_HEADER) - ) + .Where(h => !Equals(h.Key, MESSAGE_TYPE_HEADER)) .Select(h => new KeyValuePair(h.Key, DecodeHeaderValue(h.GetValueBytes()))) ); @@ -77,34 +47,8 @@ internal static MessageHeader ExtractHeaders(Headers header,out string? messageT return ExtractHeaders(header); } - internal static MessageHeader ExtractHeaders(Headers header, out string? messageTypeID,out Guid? queryClient,out Guid? replyID,out string? replyChannel) - { - messageTypeID = DecodeHeaderValue(header.FirstOrDefault(pair => Equals(pair.Key, MESSAGE_TYPE_HEADER))?.GetValueBytes()?? []); - queryClient = new Guid(header.FirstOrDefault(pair => Equals(pair.Key, QUERY_IDENTIFIER_HEADER))?.GetValueBytes()?? Guid.Empty.ToByteArray()); - replyID = new Guid(header.FirstOrDefault(pair => Equals(pair.Key, REPLY_ID))?.GetValueBytes()?? Guid.Empty.ToByteArray()); - replyChannel = DecodeHeaderValue(header.FirstOrDefault(pair => Equals(pair.Key, REPLY_CHANNEL_HEADER))?.GetValueBytes()?? []); - return ExtractHeaders(header); - } - - /// - /// Not implemented as Kafka does not support this particular action - /// - /// Throws NotImplementedException - /// Thrown because Kafka does not support this particular action - public Task PingAsync() - => throw new NotImplementedException(); - - /// - /// Called to publish a message into the Kafka server - /// - /// The service message being sent - /// The service channel options, if desired, specifically the PublishChannelOptions which is used to access the storage capabilities of KubeMQ - /// A cancellation token - /// Transmition result identifying if it worked or not - /// Thrown if options was supplied because there are no implemented options for this call - public async Task PublishAsync(ServiceMessage message, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + async ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) { - NoChannelOptionsAvailableException.ThrowIfNotNull(options); try { var result = await producer.ProduceAsync(message.Channel, new Message() @@ -121,224 +65,24 @@ public async Task PublishAsync(ServiceMessage message, IServ } } - /// - /// Called to publish a query into the Kafka server - /// - /// The service message being sent - /// The timeout supplied for the query to response - /// The options specifically for this call and must be supplied. Must be instance of QueryChannelOptions. - /// A cancellation token - /// The resulting response - /// Thrown if options is null - /// Thrown if the options that was supplied is not an instance of QueryChannelOptions - /// Thrown if the ReplyChannel is blank or null as it needs to be set - /// Thrown when the query fails to execute - /// Thrown when the responding instance has provided an error - /// Thrown when there is no response to be found for the query - public async Task QueryAsync(ServiceMessage message, TimeSpan timeout, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(options); - InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(options); - var queryChannelOptions = (QueryChannelOptions)options; - ArgumentNullException.ThrowIfNullOrWhiteSpace(queryChannelOptions.ReplyChannel); - var callID = Guid.NewGuid(); - var headers = ExtractHeaders(message, Identifier, callID, queryChannelOptions.ReplyChannel); - var tcs = StartResponseListener(clientConfig,Identifier,callID,queryChannelOptions.ReplyChannel,cancellationToken); - await producer.ProduceAsync(message.Channel, new Message() - { - Key=message.ID, - Headers=headers, - Value=message.Data.ToArray() - },cancellationToken); - try - { - await tcs.Task.WaitAsync(timeout, cancellationToken); - } - catch (Exception) - { - throw new QueryExecutionFailedException(); - } - if (tcs.Task.IsCompleted) - { - var result = tcs.Task.Result; - if (Equals(result?.MessageTypeID, ERROR_MESSAGE_TYPE_ID)) - throw new QueryAsyncReponseException(DecodeHeaderValue(result.Data.ToArray())); - else if (result!=null) - return result; - } - throw new QueryResultMissingException(); - } - - private static TaskCompletionSource StartResponseListener(ClientConfig configuration,Guid identifier,Guid callID, string replyChannel,CancellationToken cancellationToken) + async ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) { - var result = new TaskCompletionSource(); - using var queryLock = new ManualResetEventSlim(false); - Task.Run(() => - { - using var consumer = new ConsumerBuilder(new ConsumerConfig(configuration) - { - AutoOffsetReset=AutoOffsetReset.Earliest - }).Build(); - consumer.Subscribe(replyChannel); - queryLock.Set(); - while (!cancellationToken.IsCancellationRequested) - { - try - { - var msg = consumer.Consume(cancellationToken); - var headers = ExtractHeaders(msg.Message.Headers, out var messageTypeID, out var queryClient, out var replyID, out var replyChannel); - if (Equals(queryClient, identifier) && Equals(replyID, callID)) - { - Console.WriteLine(result.TrySetResult(new( - msg.Message.Key, - headers, - messageTypeID!, - msg.Message.Value - ))); - consumer.Unassign(); - break; - } - } - catch (Exception ex) { - Console.WriteLine(ex.Message); - } - } - try - { - consumer.Close(); - } - catch (Exception) { } - },cancellationToken); - queryLock.Wait(cancellationToken); - return result; - } - - /// - /// Called to create a subscription to the underlying Kafka server - /// - /// Callback for when a message is recieved - /// Callback for when an error occurs - /// The name of the channel to bind to - /// The group to subscribe as part of - /// - /// - /// - /// Thrown if options was supplied because there are no implemented options for this call - public async Task SubscribeAsync(Action messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) - { - NoChannelOptionsAvailableException.ThrowIfNotNull(options); var subscription = new PublishSubscription( new ConsumerBuilder(new ConsumerConfig(clientConfig) { - GroupId=(!string.IsNullOrWhiteSpace(group) ? group : null) + GroupId=(!string.IsNullOrWhiteSpace(group) ? group : Guid.NewGuid().ToString()) }).Build(), - messageRecieved, - errorRecieved, - channel, - cancellationToken); - subscription.Run(); - await dataLock.WaitAsync(cancellationToken); - subscriptions.Add(subscription); - dataLock.Release(); + messageReceived, + errorReceived, + channel); + await subscription.Run(); return subscription; } - /// - /// Called to create a subscription for queries to the underlying Kafka server - /// - /// Callback for when a query is recieved - /// Callback for when an error occurs - /// The name of the channel to bind to - /// The group to subscribe as part of - /// Optional QueryChannelOptions to be supplied that will specify the ReplyChannel if not supplied by query message - /// A cancellation token - /// A subscription instance - /// Thrown when options is not null and is not an instance of the type QueryChannelOptions - public async Task SubscribeQueryAsync(Func> messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) - { - InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(options); - var subscription = new QuerySubscription( - new ConsumerBuilder(new ConsumerConfig(clientConfig) - { - GroupId=(!string.IsNullOrWhiteSpace(group) ? group : null) - }).Build(), - async (recievedMessage) => - { - var headers = ExtractHeaders(recievedMessage.Headers,out var messageTypeID,out var queryClient,out var replyID,out var replyChannel); - var recievedServiceMessage = new RecievedServiceMessage( - recievedMessage.Key, - messageTypeID??string.Empty, - channel, - headers, - recievedMessage.Value - ); - try - { - var result = await messageRecieved(recievedServiceMessage); - await producer.ProduceAsync( - replyChannel??((QueryChannelOptions?)options)?.ReplyChannel??string.Empty, - new Message() - { - Key=result.ID, - Headers=ExtractHeaders(result,queryClient,replyID,replyChannel), - Value=result.Data.ToArray() - } - ); - } - catch(Exception e) - { - var respMessage = new ServiceMessage(recievedMessage.Key, ERROR_MESSAGE_TYPE_ID,replyChannel??string.Empty, new MessageHeader([]), EncodeHeaderValue(e.Message)); - await producer.ProduceAsync( - replyChannel??((QueryChannelOptions?)options)?.ReplyChannel??string.Empty, - new Message() - { - Key=recievedMessage.Key, - Headers=ExtractHeaders(respMessage,queryClient,replyID,replyChannel), - Value=respMessage.Data.ToArray() - } - ); - } - }, - errorRecieved, - channel, - cancellationToken); - subscription.Run(); - await dataLock.WaitAsync(cancellationToken); - subscriptions.Add(subscription); - dataLock.Release(); - return subscription; - } - - /// - /// Called to dispose of the resources used - /// - /// Indicates if it is disposing - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - dataLock.Wait(); - foreach (var sub in subscriptions) - sub.EndAsync().Wait(); - subscriptions.Clear(); - producer.Dispose(); - dataLock.Release(); - dataLock.Dispose(); - } - disposedValue=true; - } - } - - /// - /// Called to dispose of the resources used - /// - public void Dispose() + ValueTask IMessageServiceConnection.CloseAsync() { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + producer.Dispose(); + return ValueTask.CompletedTask; } } } diff --git a/Connectors/Kafka/Kafka.csproj b/Connectors/Kafka/Kafka.csproj index f23a1b2..d77e75d 100644 --- a/Connectors/Kafka/Kafka.csproj +++ b/Connectors/Kafka/Kafka.csproj @@ -1,26 +1,14 @@  + + - net8.0 - enable - enable - MQContract.$(MSBuildProjectName) - MQContract.$(MSBuildProjectName) - true - $(MSBuildProjectDirectory)\Readme.md - True - roger-castaldo + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) Kafka Connector for MQContract - $(AssemblyName) - https://github.com/roger-castaldo/MQContract - Readme.md - https://github.com/roger-castaldo/MQContract - Message Queue MQ Contract Kafka - MIT - True - True - True - snupkg @@ -28,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Connectors/Kafka/Options/QueryChannelOptions.cs b/Connectors/Kafka/Options/QueryChannelOptions.cs deleted file mode 100644 index 435e181..0000000 --- a/Connectors/Kafka/Options/QueryChannelOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MQContract.Interfaces.Service; - -namespace MQContract.Kafka.Options -{ - /// - /// Houses the QueryChannelOptions used for performing Query Response style messaging in Kafka - /// - /// The reply channel to use. This channel should be setup with a short retention policy, no longer than 5 minutes. - public record QueryChannelOptions(string ReplyChannel) : IServiceChannelOptions - { - } -} diff --git a/Connectors/Kafka/Readme.md b/Connectors/Kafka/Readme.md index 6124ca2..2d1a8ce 100644 --- a/Connectors/Kafka/Readme.md +++ b/Connectors/Kafka/Readme.md @@ -5,18 +5,6 @@ - [Connection](#T-MQContract-Kafka-Connection 'MQContract.Kafka.Connection') - [#ctor(clientConfig)](#M-MQContract-Kafka-Connection-#ctor-Confluent-Kafka-ClientConfig- 'MQContract.Kafka.Connection.#ctor(Confluent.Kafka.ClientConfig)') - - [DefaultTimout](#P-MQContract-Kafka-Connection-DefaultTimout 'MQContract.Kafka.Connection.DefaultTimout') - - [MaxMessageBodySize](#P-MQContract-Kafka-Connection-MaxMessageBodySize 'MQContract.Kafka.Connection.MaxMessageBodySize') - - [Dispose(disposing)](#M-MQContract-Kafka-Connection-Dispose-System-Boolean- 'MQContract.Kafka.Connection.Dispose(System.Boolean)') - - [Dispose()](#M-MQContract-Kafka-Connection-Dispose 'MQContract.Kafka.Connection.Dispose') - - [PingAsync()](#M-MQContract-Kafka-Connection-PingAsync 'MQContract.Kafka.Connection.PingAsync') - - [PublishAsync(message,options,cancellationToken)](#M-MQContract-Kafka-Connection-PublishAsync-MQContract-Messages-ServiceMessage,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Kafka.Connection.PublishAsync(MQContract.Messages.ServiceMessage,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync(message,timeout,options,cancellationToken)](#M-MQContract-Kafka-Connection-QueryAsync-MQContract-Messages-ServiceMessage,System-TimeSpan,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Kafka.Connection.QueryAsync(MQContract.Messages.ServiceMessage,System.TimeSpan,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-Kafka-Connection-SubscribeAsync-System-Action{MQContract-Messages-RecievedServiceMessage},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Kafka.Connection.SubscribeAsync(System.Action{MQContract.Messages.RecievedServiceMessage},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-Kafka-Connection-SubscribeQueryAsync-System-Func{MQContract-Messages-RecievedServiceMessage,System-Threading-Tasks-Task{MQContract-Messages-ServiceMessage}},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.Kafka.Connection.SubscribeQueryAsync(System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') -- [QueryChannelOptions](#T-MQContract-Kafka-Options-QueryChannelOptions 'MQContract.Kafka.Options.QueryChannelOptions') - - [#ctor(ReplyChannel)](#M-MQContract-Kafka-Options-QueryChannelOptions-#ctor-System-String- 'MQContract.Kafka.Options.QueryChannelOptions.#ctor(System.String)') - - [ReplyChannel](#P-MQContract-Kafka-Options-QueryChannelOptions-ReplyChannel 'MQContract.Kafka.Options.QueryChannelOptions.ReplyChannel') ## Connection `type` @@ -33,7 +21,7 @@ This is the MessageServiceConnection implementation for using Kafka | Name | Type | Description | | ---- | ---- | ----------- | -| clientConfig | [T:MQContract.Kafka.Connection](#T-T-MQContract-Kafka-Connection 'T:MQContract.Kafka.Connection') | | +| clientConfig | [T:MQContract.Kafka.Connection](#T-T-MQContract-Kafka-Connection 'T:MQContract.Kafka.Connection') | The Kafka Client Configuration to provide | ### #ctor(clientConfig) `constructor` @@ -46,213 +34,4 @@ This is the MessageServiceConnection implementation for using Kafka | Name | Type | Description | | ---- | ---- | ----------- | -| clientConfig | [Confluent.Kafka.ClientConfig](#T-Confluent-Kafka-ClientConfig 'Confluent.Kafka.ClientConfig') | | - - -### DefaultTimout `property` - -##### Summary - -The default timeout to use for RPC calls when not specified by the class or in the call. -DEFAULT:1 minute if not specified inside the connection options - - -### MaxMessageBodySize `property` - -##### Summary - -The maximum message body size allowed - - -### Dispose(disposing) `method` - -##### Summary - -Called to dispose of the resources used - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| disposing | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Indicates if it is disposing | - - -### Dispose() `method` - -##### Summary - -Called to dispose of the resources used - -##### Parameters - -This method has no parameters. - - -### PingAsync() `method` - -##### Summary - -Not implemented as Kafka does not support this particular action - -##### Returns - -Throws NotImplementedException - -##### Parameters - -This method has no parameters. - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [System.NotImplementedException](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.NotImplementedException 'System.NotImplementedException') | Thrown because Kafka does not support this particular action | - - -### PublishAsync(message,options,cancellationToken) `method` - -##### Summary - -Called to publish a message into the Kafka server - -##### Returns - -Transmition result identifying if it worked or not - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message being sent | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The service channel options, if desired, specifically the PublishChannelOptions which is used to access the storage capabilities of KubeMQ | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown if options was supplied because there are no implemented options for this call | - - -### QueryAsync(message,timeout,options,cancellationToken) `method` - -##### Summary - -Called to publish a query into the Kafka server - -##### Returns - -The resulting response - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message being sent | -| timeout | [System.TimeSpan](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.TimeSpan 'System.TimeSpan') | The timeout supplied for the query to response | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The options specifically for this call and must be supplied. Must be instance of QueryChannelOptions. | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [System.ArgumentNullException](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.ArgumentNullException 'System.ArgumentNullException') | Thrown if options is null | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown if the options that was supplied is not an instance of QueryChannelOptions | -| [System.ArgumentNullException](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.ArgumentNullException 'System.ArgumentNullException') | Thrown if the ReplyChannel is blank or null as it needs to be set | -| [MQContract.Kafka.QueryExecutionFailedException](#T-MQContract-Kafka-QueryExecutionFailedException 'MQContract.Kafka.QueryExecutionFailedException') | Thrown when the query fails to execute | -| [MQContract.Kafka.QueryAsyncReponseException](#T-MQContract-Kafka-QueryAsyncReponseException 'MQContract.Kafka.QueryAsyncReponseException') | Thrown when the responding instance has provided an error | -| [MQContract.Kafka.QueryResultMissingException](#T-MQContract-Kafka-QueryResultMissingException 'MQContract.Kafka.QueryResultMissingException') | Thrown when there is no response to be found for the query | - - -### SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` - -##### Summary - -Called to create a subscription to the underlying Kafka server - -##### Returns - - - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Action{MQContract.Messages.RecievedServiceMessage}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Messages.RecievedServiceMessage}') | Callback for when a message is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | Callback for when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to bind to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The group to subscribe as part of | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown if options was supplied because there are no implemented options for this call | - - -### SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` - -##### Summary - -Called to create a subscription for queries to the underlying Kafka server - -##### Returns - -A subscription instance - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}') | Callback for when a query is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | Callback for when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to bind to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The group to subscribe as part of | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Optional QueryChannelOptions to be supplied that will specify the ReplyChannel if not supplied by query message | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when options is not null and is not an instance of the type QueryChannelOptions | - - -## QueryChannelOptions `type` - -##### Namespace - -MQContract.Kafka.Options - -##### Summary - -Houses the QueryChannelOptions used for performing Query Response style messaging in Kafka - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| ReplyChannel | [T:MQContract.Kafka.Options.QueryChannelOptions](#T-T-MQContract-Kafka-Options-QueryChannelOptions 'T:MQContract.Kafka.Options.QueryChannelOptions') | The reply channel to use. This channel should be setup with a short retention policy, no longer than 5 minutes. | - - -### #ctor(ReplyChannel) `constructor` - -##### Summary - -Houses the QueryChannelOptions used for performing Query Response style messaging in Kafka - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| ReplyChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The reply channel to use. This channel should be setup with a short retention policy, no longer than 5 minutes. | - - -### ReplyChannel `property` - -##### Summary - -The reply channel to use. This channel should be setup with a short retention policy, no longer than 5 minutes. +| clientConfig | [Confluent.Kafka.ClientConfig](#T-Confluent-Kafka-ClientConfig 'Confluent.Kafka.ClientConfig') | The Kafka Client Configuration to provide | diff --git a/Connectors/Kafka/Subscriptions/PublishSubscription.cs b/Connectors/Kafka/Subscriptions/PublishSubscription.cs index 0df5d5c..16e1dbe 100644 --- a/Connectors/Kafka/Subscriptions/PublishSubscription.cs +++ b/Connectors/Kafka/Subscriptions/PublishSubscription.cs @@ -1,12 +1,11 @@ using MQContract.Messages; -using MQContract.NATS.Subscriptions; namespace MQContract.Kafka.Subscriptions { - internal class PublishSubscription(Confluent.Kafka.IConsumer consumer, Action messageRecieved, Action errorRecieved, string channel, CancellationToken cancellationToken) - : SubscriptionBase(consumer,channel,cancellationToken) + internal class PublishSubscription(Confluent.Kafka.IConsumer consumer, Action messageReceived, Action errorReceived, string channel) + : SubscriptionBase(consumer,channel) { - protected override Task RunAction() + protected override ValueTask RunAction() { while (!cancelToken.IsCancellationRequested) { @@ -14,7 +13,7 @@ protected override Task RunAction() { var msg = Consumer.Consume(cancellationToken:cancelToken.Token); var headers = Connection.ExtractHeaders(msg.Message.Headers, out var messageTypeID); - messageRecieved(new RecievedServiceMessage( + messageReceived(new ReceivedServiceMessage( msg.Message.Key??string.Empty, messageTypeID??string.Empty, Channel, @@ -25,11 +24,11 @@ protected override Task RunAction() catch (OperationCanceledException) { } catch (Exception ex) { - errorRecieved(ex); + errorReceived(ex); } finally { } } - return Task.CompletedTask; + return ValueTask.CompletedTask; } } } diff --git a/Connectors/Kafka/Subscriptions/QuerySubscription.cs b/Connectors/Kafka/Subscriptions/QuerySubscription.cs deleted file mode 100644 index 55e2a5e..0000000 --- a/Connectors/Kafka/Subscriptions/QuerySubscription.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Confluent.Kafka; -using MQContract.NATS.Subscriptions; - -namespace MQContract.Kafka.Subscriptions -{ - internal class QuerySubscription(Confluent.Kafka.IConsumer consumer, Func,Task> messageRecieved, Action errorRecieved, string channel, CancellationToken cancellationToken) - : SubscriptionBase(consumer,channel,cancellationToken) - { - protected override async Task RunAction() - { - while (!cancelToken.IsCancellationRequested) - { - try - { - var msg = Consumer.Consume(); - await messageRecieved(msg.Message); - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - errorRecieved(ex); - } - } - } - } -} diff --git a/Connectors/Kafka/Subscriptions/SubscriptionBase.cs b/Connectors/Kafka/Subscriptions/SubscriptionBase.cs index 6525e0d..726c598 100644 --- a/Connectors/Kafka/Subscriptions/SubscriptionBase.cs +++ b/Connectors/Kafka/Subscriptions/SubscriptionBase.cs @@ -1,58 +1,48 @@ using MQContract.Interfaces.Service; -namespace MQContract.NATS.Subscriptions +namespace MQContract.Kafka.Subscriptions { - internal abstract class SubscriptionBase(Confluent.Kafka.IConsumer consumer,string channel,CancellationToken cancellationToken) : IServiceSubscription + internal abstract class SubscriptionBase(Confluent.Kafka.IConsumer consumer,string channel) : IServiceSubscription { protected readonly Confluent.Kafka.IConsumer Consumer = consumer; protected readonly string Channel = channel; private bool disposedValue; protected readonly CancellationTokenSource cancelToken = new(); - protected abstract Task RunAction(); - public void Run() + protected abstract ValueTask RunAction(); + public Task Run() { - cancellationToken.Register(() => - { - cancelToken.Cancel(); - }); + var resultSource = new TaskCompletionSource(); Task.Run(async () => { Consumer.Subscribe(Channel); + resultSource.TrySetResult(); await RunAction(); Consumer.Close(); }); + return resultSource.Task; } - public async Task EndAsync() + public async ValueTask EndAsync() { try { await cancelToken.CancelAsync(); } catch { } } - protected virtual void Dispose(bool disposing) + public async ValueTask DisposeAsync() { if (!disposedValue) { - if (disposing) + disposedValue=true; + if (!cancelToken.IsCancellationRequested) + await cancelToken.CancelAsync(); + try { - if (!cancellationToken.IsCancellationRequested) - cancelToken.Cancel(); - try - { - Consumer.Close(); - } - catch (Exception) { } - Consumer.Dispose(); - cancelToken.Dispose(); + Consumer.Close(); } - disposedValue=true; + catch (Exception) { } + Consumer.Dispose(); + cancelToken.Dispose(); } } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/Connectors/KubeMQ/Connection.cs b/Connectors/KubeMQ/Connection.cs index 96151de..4e0a169 100644 --- a/Connectors/KubeMQ/Connection.cs +++ b/Connectors/KubeMQ/Connection.cs @@ -17,14 +17,44 @@ namespace MQContract.KubeMQ /// /// This is the MessageServiceConnection implementation for using KubeMQ /// - public class Connection : IMessageServiceConnection + public sealed class Connection : IQueryResponseMessageServiceConnection,IPingableMessageServiceConnection, IDisposable,IAsyncDisposable { + /// + /// These are the different read styles to use when subscribing to a stored Event PubSub + /// + public enum MessageReadStyle + { + /// + /// Start from the new ones (unread ones) only + /// + StartNewOnly = 1, + /// + /// Start at the beginning + /// + StartFromFirst = 2, + /// + /// Start at the last message + /// + StartFromLast = 3, + /// + /// Start at message number X (this value is specified when creating the listener) + /// + StartAtSequence = 4, + /// + /// Start at time X (this value is specified when creating the listener) + /// + StartAtTime = 5, + /// + /// Start at Time Delte X (this value is specified when creating the listener) + /// + StartAtTimeDelta = 6 + }; + private static readonly Regex regURL = new("^http(s)?://(.+)$", RegexOptions.Compiled|RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(500)); private readonly ConnectionOptions connectionOptions; private readonly KubeClient client; - private readonly List subscriptions = []; - private readonly SemaphoreSlim dataLock = new(1, 1); + private readonly List storedChannelOptions = []; private bool disposedValue; /// @@ -58,15 +88,44 @@ public Connection(ConnectionOptions options) } /// - /// The maximum message body size allowed + /// Called to flag a particular channel as Stored Events when publishing or subscribing + /// + /// The name of the channel + /// The current connection to allow for chaining + public Connection RegisterStoredChannel(string channelName) + { + storedChannelOptions.Add(new(channelName)); + return this; + } + + /// + /// Called to flag a particular channel as Stored Events when publishing or subscribing /// - public int? MaxMessageBodySize => connectionOptions.MaxBodySize; + /// The name of the channel + /// Set the message reading style when subscribing + /// The current connection to allow for chaining + public Connection RegisterStoredChannel(string channelName, MessageReadStyle readStyle) + { + storedChannelOptions.Add(new(channelName, readStyle)); + return this; + } /// - /// The default timeout to use for RPC calls when not specified by the class or in the call. - /// DEFAULT:30 seconds if not specified inside the connection options + /// Called to flag a particular channel as Stored Events when publishing or subscribing /// - public TimeSpan DefaultTimout => TimeSpan.FromMilliseconds(connectionOptions.DefaultRPCTimeout??30000); + /// The name of the channel + /// Set the message reading style when subscribing + /// Set the readoffset to use for the given reading style + /// The current connection to allow for chaining + public Connection RegisterStoredChannel(string channelName, MessageReadStyle readStyle, long readOffset) + { + storedChannelOptions.Add(new(channelName, readStyle, readOffset)); + return this; + } + + uint? IMessageServiceConnection.MaxMessageBodySize => (uint)Math.Abs(connectionOptions.MaxBodySize); + + TimeSpan IQueryableMessageServiceConnection.DefaultTimeout => TimeSpan.FromMilliseconds(connectionOptions.DefaultRPCTimeout??30000); private KubeClient EstablishConnection() { @@ -76,18 +135,13 @@ private KubeClient EstablishConnection() return result; } - /// - /// Called to ping the KubeMQ service - /// - /// The Ping result, specically a PingResponse instance - /// Thrown when the Ping fails - public Task PingAsync() + ValueTask IPingableMessageServiceConnection.PingAsync() { var watch = new Stopwatch(); watch.Start(); var res = client.Ping()??throw new UnableToConnectException(); watch.Stop(); - return Task.FromResult(new PingResponse(res,watch.Elapsed)); + return ValueTask.FromResult(new PingResponse(res,watch.Elapsed)); } internal static MapField ConvertMessageHeader(MessageHeader header) @@ -101,17 +155,8 @@ internal static MapField ConvertMessageHeader(MessageHeader head internal static MessageHeader ConvertMessageHeader(MapField header) => new(header.AsEnumerable()); - /// - /// Called to publish a message into the KubeMQ server - /// - /// The service message being sent - /// The service channel options, if desired, specifically the PublishChannelOptions which is used to access the storage capabilities of KubeMQ - /// A cancellation token - /// Transmition result identifying if it worked or not - /// /// Thrown when an attempt to pass an options object that is not of the type PublishChannelOptions - public async Task PublishAsync(ServiceMessage message, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + async ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) { - InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(options); try { var res = await client.SendEventAsync(new Event() { @@ -120,7 +165,7 @@ public async Task PublishAsync(ServiceMessage message, IServ Channel=message.Channel, ClientID=connectionOptions.ClientId, EventID=message.ID, - Store=(options is PublishChannelOptions pbc && pbc.Stored), + Store=storedChannelOptions.Exists(sco=>Equals(message.Channel,sco.ChannelName)), Tags={ ConvertMessageHeader(message.Header) } }, connectionOptions.GrpcMetadata, cancellationToken); return new TransmissionResult(res.EventID, res.Error); @@ -137,20 +182,9 @@ public async Task PublishAsync(ServiceMessage message, IServ } } - /// - /// Called to publish a query into the KubeMQ server - /// - /// The service message being sent - /// The timeout supplied for the query to response - /// Should be null here as there is no Service Channel Options implemented for this call - /// A cancellation token - /// The resulting response - /// Thrown if options was supplied because there are no implemented options for this call - /// Thrown when the response from KubeMQ is null - /// Thrown when there is an RPC exception from the KubeMQ server - public async Task QueryAsync(ServiceMessage message, TimeSpan timeout, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + async ValueTask IQueryResponseMessageServiceConnection.QueryAsync(ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) { - NoChannelOptionsAvailableException.ThrowIfNotNull(options); +#pragma warning disable S2139 // Exceptions should be either logged or rethrown but not both try { var res = await client.SendRequestAsync(new Request() @@ -182,94 +216,62 @@ public async Task QueryAsync(ServiceMessage message, TimeSpa connectionOptions.Logger?.LogError(ex, "Exception occured in Send Message:{ErrorMessage}", ex.Message); throw; } +#pragma warning restore S2139 // Exceptions should be either logged or rethrown but not both } - /// - /// Called to create a subscription to the underlying KubeMQ server - /// - /// Callback for when a message is recieved - /// Callback for when an error occurs - /// The name of the channel to bind to - /// The group to subscribe as part of - /// The service channel options, if desired, specifically the StoredEventsSubscriptionOptions which is used to access stored event streams - /// A cancellation token - /// A subscription instance - /// Thrown when options is not null and is not an instance of the type StoredEventsSubscriptionOptions - public async Task SubscribeAsync(Action messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) { - InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(options); var sub = new PubSubscription( connectionOptions, EstablishConnection(), - messageRecieved, - errorRecieved, + messageReceived, + errorReceived, channel, - group, - (StoredEventsSubscriptionOptions?)options, + group??Guid.NewGuid().ToString(), + storedChannelOptions.Find(sco=>Equals(sco.ChannelName,channel)), cancellationToken ); sub.Run(); - await dataLock.WaitAsync(cancellationToken); - subscriptions.Add(sub); - dataLock.Release(); - return sub; + return ValueTask.FromResult(sub); } - /// - /// Called to create a subscription for queries to the underlying KubeMQ server - /// - /// Callback for when a query is recieved - /// Callback for when an error occurs - /// The name of the channel to bind to - /// The group to subscribe as part of - /// Should be null here as there is no Service Channel Options implemented for this call - /// A cancellation token - /// A subscription instance - /// Thrown if options was supplied because there are no implemented options for this call - public async Task SubscribeQueryAsync(Func> messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + ValueTask IQueryableMessageServiceConnection.SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) { - NoChannelOptionsAvailableException.ThrowIfNotNull(options); var sub = new QuerySubscription( connectionOptions, EstablishConnection(), - messageRecieved, - errorRecieved, + messageReceived, + errorReceived, channel, - group, + group??Guid.NewGuid().ToString(), cancellationToken ); sub.Run(); - await dataLock.WaitAsync(cancellationToken); - subscriptions.Add(sub); - dataLock.Release(); - return sub; + return ValueTask.FromResult(sub); } - /// - /// Called to dispose of the resources used - /// - /// Indicates if it is disposing - protected virtual void Dispose(bool disposing) + + ValueTask IMessageServiceConnection.CloseAsync() + => client.DisposeAsync(); + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await client.DisposeAsync(); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) { if (!disposedValue) { if (disposing) - { - dataLock.Wait(); - foreach (var sub in subscriptions) - sub.EndAsync().Wait(); - subscriptions.Clear(); - client.Dispose(); - dataLock.Release(); - dataLock.Dispose(); - } + client.DisposeAsync().AsTask().Wait(); disposedValue=true; } } - - /// - /// Called to dispose of the resources used - /// - public void Dispose() + + void IDisposable.Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); diff --git a/Connectors/KubeMQ/Exceptions.cs b/Connectors/KubeMQ/Exceptions.cs index 41060b3..08ea47d 100644 --- a/Connectors/KubeMQ/Exceptions.cs +++ b/Connectors/KubeMQ/Exceptions.cs @@ -33,7 +33,7 @@ internal MessageResponseTransmissionException(Guid subscriptionID,string message internal class NullResponseException : NullReferenceException { internal NullResponseException() - : base("null response recieved from KubeMQ server") { } + : base("null response received from KubeMQ server") { } } internal class RPCErrorException : Exception diff --git a/Connectors/KubeMQ/KubeMQ.csproj b/Connectors/KubeMQ/KubeMQ.csproj index cacaa90..0f8bf69 100644 --- a/Connectors/KubeMQ/KubeMQ.csproj +++ b/Connectors/KubeMQ/KubeMQ.csproj @@ -1,25 +1,14 @@  + + - net8.0 - enable - enable - MQContract.$(MSBuildProjectName) - MQContract.$(MSBuildProjectName) - true - $(MSBuildProjectDirectory)\Readme.md - roger-castaldo + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) KubeMQ Connector for MQContract - $(AssemblyName) - https://github.com/roger-castaldo/MQContract - Readme.md - https://github.com/roger-castaldo/MQContract - Message Queue MQ Contract KubeMQ - MIT - True - True - True - snupkg @@ -27,9 +16,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Connectors/KubeMQ/Options/PublishChannelOptions.cs b/Connectors/KubeMQ/Options/PublishChannelOptions.cs deleted file mode 100644 index 8f9fd34..0000000 --- a/Connectors/KubeMQ/Options/PublishChannelOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MQContract.Interfaces.Service; - -namespace MQContract.KubeMQ.Options -{ - /// - /// Houses the Publish Channel options used when calling the Publish command - /// - /// Indicates if the publish should be using storage - public record PublishChannelOptions(bool Stored) : IServiceChannelOptions - { - } -} diff --git a/Connectors/KubeMQ/Options/StoredChannelOptions.cs b/Connectors/KubeMQ/Options/StoredChannelOptions.cs new file mode 100644 index 0000000..b5649c7 --- /dev/null +++ b/Connectors/KubeMQ/Options/StoredChannelOptions.cs @@ -0,0 +1,7 @@ +using static MQContract.KubeMQ.Connection; + +namespace MQContract.KubeMQ.Options +{ + internal record StoredChannelOptions(string ChannelName,MessageReadStyle ReadStyle=MessageReadStyle.StartNewOnly,long ReadOffset=0) + {} +} diff --git a/Connectors/KubeMQ/Options/StoredEventsSubscriptionOptions.cs b/Connectors/KubeMQ/Options/StoredEventsSubscriptionOptions.cs deleted file mode 100644 index bf69f9b..0000000 --- a/Connectors/KubeMQ/Options/StoredEventsSubscriptionOptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MQContract.Interfaces.Service; -using static MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions; - -namespace MQContract.KubeMQ.Options -{ - /// - /// Houses the configuration for a subscription going to a stored message channel - /// - /// The read style to use - /// The read offset to use - public record StoredEventsSubscriptionOptions(MessageReadStyle ReadStyle=MessageReadStyle.StartNewOnly,long ReadOffset=0) : IServiceChannelOptions - { - /// - /// These are the different read styles to use when subscribing to a stored Event PubSub - /// - public enum MessageReadStyle - { - /// - /// Start from the new ones (unread ones) only - /// - StartNewOnly = 1, - /// - /// Start at the beginning - /// - StartFromFirst = 2, - /// - /// Start at the last message - /// - StartFromLast = 3, - /// - /// Start at message number X (this value is specified when creating the listener) - /// - StartAtSequence = 4, - /// - /// Start at time X (this value is specified when creating the listener) - /// - StartAtTime = 5, - /// - /// Start at Time Delte X (this value is specified when creating the listener) - /// - StartAtTimeDelta = 6 - }; - - } -} diff --git a/Connectors/KubeMQ/Readme.md b/Connectors/KubeMQ/Readme.md index 7bac41d..b3ea5b5 100644 --- a/Connectors/KubeMQ/Readme.md +++ b/Connectors/KubeMQ/Readme.md @@ -16,15 +16,9 @@ - [ClientDisposedException](#T-MQContract-KubeMQ-ClientDisposedException 'MQContract.KubeMQ.ClientDisposedException') - [Connection](#T-MQContract-KubeMQ-Connection 'MQContract.KubeMQ.Connection') - [#ctor(options)](#M-MQContract-KubeMQ-Connection-#ctor-MQContract-KubeMQ-ConnectionOptions- 'MQContract.KubeMQ.Connection.#ctor(MQContract.KubeMQ.ConnectionOptions)') - - [DefaultTimout](#P-MQContract-KubeMQ-Connection-DefaultTimout 'MQContract.KubeMQ.Connection.DefaultTimout') - - [MaxMessageBodySize](#P-MQContract-KubeMQ-Connection-MaxMessageBodySize 'MQContract.KubeMQ.Connection.MaxMessageBodySize') - - [Dispose(disposing)](#M-MQContract-KubeMQ-Connection-Dispose-System-Boolean- 'MQContract.KubeMQ.Connection.Dispose(System.Boolean)') - - [Dispose()](#M-MQContract-KubeMQ-Connection-Dispose 'MQContract.KubeMQ.Connection.Dispose') - - [PingAsync()](#M-MQContract-KubeMQ-Connection-PingAsync 'MQContract.KubeMQ.Connection.PingAsync') - - [PublishAsync(message,options,cancellationToken)](#M-MQContract-KubeMQ-Connection-PublishAsync-MQContract-Messages-ServiceMessage,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.KubeMQ.Connection.PublishAsync(MQContract.Messages.ServiceMessage,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync(message,timeout,options,cancellationToken)](#M-MQContract-KubeMQ-Connection-QueryAsync-MQContract-Messages-ServiceMessage,System-TimeSpan,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.KubeMQ.Connection.QueryAsync(MQContract.Messages.ServiceMessage,System.TimeSpan,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-KubeMQ-Connection-SubscribeAsync-System-Action{MQContract-Messages-RecievedServiceMessage},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.KubeMQ.Connection.SubscribeAsync(System.Action{MQContract.Messages.RecievedServiceMessage},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-KubeMQ-Connection-SubscribeQueryAsync-System-Func{MQContract-Messages-RecievedServiceMessage,System-Threading-Tasks-Task{MQContract-Messages-ServiceMessage}},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.KubeMQ.Connection.SubscribeQueryAsync(System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') + - [RegisterStoredChannel(channelName)](#M-MQContract-KubeMQ-Connection-RegisterStoredChannel-System-String- 'MQContract.KubeMQ.Connection.RegisterStoredChannel(System.String)') + - [RegisterStoredChannel(channelName,readStyle)](#M-MQContract-KubeMQ-Connection-RegisterStoredChannel-System-String,MQContract-KubeMQ-Connection-MessageReadStyle- 'MQContract.KubeMQ.Connection.RegisterStoredChannel(System.String,MQContract.KubeMQ.Connection.MessageReadStyle)') + - [RegisterStoredChannel(channelName,readStyle,readOffset)](#M-MQContract-KubeMQ-Connection-RegisterStoredChannel-System-String,MQContract-KubeMQ-Connection-MessageReadStyle,System-Int64- 'MQContract.KubeMQ.Connection.RegisterStoredChannel(System.String,MQContract.KubeMQ.Connection.MessageReadStyle,System.Int64)') - [ConnectionOptions](#T-MQContract-KubeMQ-ConnectionOptions 'MQContract.KubeMQ.ConnectionOptions') - [Address](#P-MQContract-KubeMQ-ConnectionOptions-Address 'MQContract.KubeMQ.ConnectionOptions.Address') - [AuthToken](#P-MQContract-KubeMQ-ConnectionOptions-AuthToken 'MQContract.KubeMQ.ConnectionOptions.AuthToken') @@ -60,13 +54,13 @@ - [Version](#P-MQContract-KubeMQ-Interfaces-IKubeMQPingResult-Version 'MQContract.KubeMQ.Interfaces.IKubeMQPingResult.Version') - [KubemqReflection](#T-MQContract-KubeMQ-SDK-Grpc-KubemqReflection 'MQContract.KubeMQ.SDK.Grpc.KubemqReflection') - [Descriptor](#P-MQContract-KubeMQ-SDK-Grpc-KubemqReflection-Descriptor 'MQContract.KubeMQ.SDK.Grpc.KubemqReflection.Descriptor') -- [MessageReadStyle](#T-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle') - - [StartAtSequence](#F-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle-StartAtSequence 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle.StartAtSequence') - - [StartAtTime](#F-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle-StartAtTime 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle.StartAtTime') - - [StartAtTimeDelta](#F-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle-StartAtTimeDelta 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle.StartAtTimeDelta') - - [StartFromFirst](#F-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle-StartFromFirst 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle.StartFromFirst') - - [StartFromLast](#F-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle-StartFromLast 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle.StartFromLast') - - [StartNewOnly](#F-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle-StartNewOnly 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle.StartNewOnly') +- [MessageReadStyle](#T-MQContract-KubeMQ-Connection-MessageReadStyle 'MQContract.KubeMQ.Connection.MessageReadStyle') + - [StartAtSequence](#F-MQContract-KubeMQ-Connection-MessageReadStyle-StartAtSequence 'MQContract.KubeMQ.Connection.MessageReadStyle.StartAtSequence') + - [StartAtTime](#F-MQContract-KubeMQ-Connection-MessageReadStyle-StartAtTime 'MQContract.KubeMQ.Connection.MessageReadStyle.StartAtTime') + - [StartAtTimeDelta](#F-MQContract-KubeMQ-Connection-MessageReadStyle-StartAtTimeDelta 'MQContract.KubeMQ.Connection.MessageReadStyle.StartAtTimeDelta') + - [StartFromFirst](#F-MQContract-KubeMQ-Connection-MessageReadStyle-StartFromFirst 'MQContract.KubeMQ.Connection.MessageReadStyle.StartFromFirst') + - [StartFromLast](#F-MQContract-KubeMQ-Connection-MessageReadStyle-StartFromLast 'MQContract.KubeMQ.Connection.MessageReadStyle.StartFromLast') + - [StartNewOnly](#F-MQContract-KubeMQ-Connection-MessageReadStyle-StartNewOnly 'MQContract.KubeMQ.Connection.MessageReadStyle.StartNewOnly') - [MessageResponseTransmissionException](#T-MQContract-KubeMQ-MessageResponseTransmissionException 'MQContract.KubeMQ.MessageResponseTransmissionException') - [PingResult](#T-MQContract-KubeMQ-SDK-Grpc-PingResult 'MQContract.KubeMQ.SDK.Grpc.PingResult') - [HostFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-PingResult-HostFieldNumber 'MQContract.KubeMQ.SDK.Grpc.PingResult.HostFieldNumber') @@ -90,9 +84,6 @@ - [RefRequestIdFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-PollResponse-RefRequestIdFieldNumber 'MQContract.KubeMQ.SDK.Grpc.PollResponse.RefRequestIdFieldNumber') - [StreamRequestTypeDataFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-PollResponse-StreamRequestTypeDataFieldNumber 'MQContract.KubeMQ.SDK.Grpc.PollResponse.StreamRequestTypeDataFieldNumber') - [TransactionIdFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-PollResponse-TransactionIdFieldNumber 'MQContract.KubeMQ.SDK.Grpc.PollResponse.TransactionIdFieldNumber') -- [PublishChannelOptions](#T-MQContract-KubeMQ-Options-PublishChannelOptions 'MQContract.KubeMQ.Options.PublishChannelOptions') - - [#ctor(Stored)](#M-MQContract-KubeMQ-Options-PublishChannelOptions-#ctor-System-Boolean- 'MQContract.KubeMQ.Options.PublishChannelOptions.#ctor(System.Boolean)') - - [Stored](#P-MQContract-KubeMQ-Options-PublishChannelOptions-Stored 'MQContract.KubeMQ.Options.PublishChannelOptions.Stored') - [QueueMessage](#T-MQContract-KubeMQ-SDK-Grpc-QueueMessage 'MQContract.KubeMQ.SDK.Grpc.QueueMessage') - [AttributesFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-QueueMessage-AttributesFieldNumber 'MQContract.KubeMQ.SDK.Grpc.QueueMessage.AttributesFieldNumber') - [BodyFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-QueueMessage-BodyFieldNumber 'MQContract.KubeMQ.SDK.Grpc.QueueMessage.BodyFieldNumber') @@ -174,10 +165,6 @@ - [IsErrorFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-SendQueueMessageResult-IsErrorFieldNumber 'MQContract.KubeMQ.SDK.Grpc.SendQueueMessageResult.IsErrorFieldNumber') - [MessageIDFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-SendQueueMessageResult-MessageIDFieldNumber 'MQContract.KubeMQ.SDK.Grpc.SendQueueMessageResult.MessageIDFieldNumber') - [SentAtFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-SendQueueMessageResult-SentAtFieldNumber 'MQContract.KubeMQ.SDK.Grpc.SendQueueMessageResult.SentAtFieldNumber') -- [StoredEventsSubscriptionOptions](#T-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions') - - [#ctor(ReadStyle,ReadOffset)](#M-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-#ctor-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle,System-Int64- 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.#ctor(MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle,System.Int64)') - - [ReadOffset](#P-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-ReadOffset 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.ReadOffset') - - [ReadStyle](#P-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-ReadStyle 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.ReadStyle') - [StreamQueueMessagesRequest](#T-MQContract-KubeMQ-SDK-Grpc-StreamQueueMessagesRequest 'MQContract.KubeMQ.SDK.Grpc.StreamQueueMessagesRequest') - [ChannelFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-StreamQueueMessagesRequest-ChannelFieldNumber 'MQContract.KubeMQ.SDK.Grpc.StreamQueueMessagesRequest.ChannelFieldNumber') - [ClientIDFieldNumber](#F-MQContract-KubeMQ-SDK-Grpc-StreamQueueMessagesRequest-ClientIDFieldNumber 'MQContract.KubeMQ.SDK.Grpc.StreamQueueMessagesRequest.ClientIDFieldNumber') @@ -323,174 +310,59 @@ Primary constructor to create an instance using the supplied configuration optio | ---- | ----------- | | [MQContract.KubeMQ.UnableToConnectException](#T-MQContract-KubeMQ-UnableToConnectException 'MQContract.KubeMQ.UnableToConnectException') | Thrown when the initial attempt to connect fails | - -### DefaultTimout `property` + +### RegisterStoredChannel(channelName) `method` ##### Summary -The default timeout to use for RPC calls when not specified by the class or in the call. -DEFAULT:30 seconds if not specified inside the connection options - - -### MaxMessageBodySize `property` - -##### Summary - -The maximum message body size allowed - - -### Dispose(disposing) `method` - -##### Summary - -Called to dispose of the resources used - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| disposing | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Indicates if it is disposing | - - -### Dispose() `method` - -##### Summary - -Called to dispose of the resources used - -##### Parameters - -This method has no parameters. - - -### PingAsync() `method` - -##### Summary - -Called to ping the KubeMQ service - -##### Returns - -The Ping result, specically a PingResponse instance - -##### Parameters - -This method has no parameters. - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.KubeMQ.UnableToConnectException](#T-MQContract-KubeMQ-UnableToConnectException 'MQContract.KubeMQ.UnableToConnectException') | Thrown when the Ping fails | - - -### PublishAsync(message,options,cancellationToken) `method` - -##### Summary - -Called to publish a message into the KubeMQ server +Called to flag a particular channel as Stored Events when publishing or subscribing ##### Returns -Transmition result identifying if it worked or not +The current connection to allow for chaining ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message being sent | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The service channel options, if desired, specifically the PublishChannelOptions which is used to access the storage capabilities of KubeMQ | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when an attempt to pass an options object that is not of the type PublishChannelOptions | +| channelName | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel | - -### QueryAsync(message,timeout,options,cancellationToken) `method` + +### RegisterStoredChannel(channelName,readStyle) `method` ##### Summary -Called to publish a query into the KubeMQ server +Called to flag a particular channel as Stored Events when publishing or subscribing ##### Returns -The resulting response +The current connection to allow for chaining ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message being sent | -| timeout | [System.TimeSpan](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.TimeSpan 'System.TimeSpan') | The timeout supplied for the query to response | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Should be null here as there is no Service Channel Options implemented for this call | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions +| channelName | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel | +| readStyle | [MQContract.KubeMQ.Connection.MessageReadStyle](#T-MQContract-KubeMQ-Connection-MessageReadStyle 'MQContract.KubeMQ.Connection.MessageReadStyle') | Set the message reading style when subscribing | -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown if options was supplied because there are no implemented options for this call | -| [MQContract.KubeMQ.NullResponseException](#T-MQContract-KubeMQ-NullResponseException 'MQContract.KubeMQ.NullResponseException') | Thrown when the response from KubeMQ is null | -| [MQContract.KubeMQ.RPCErrorException](#T-MQContract-KubeMQ-RPCErrorException 'MQContract.KubeMQ.RPCErrorException') | Thrown when there is an RPC exception from the KubeMQ server | - - -### SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` + +### RegisterStoredChannel(channelName,readStyle,readOffset) `method` ##### Summary -Called to create a subscription to the underlying KubeMQ server +Called to flag a particular channel as Stored Events when publishing or subscribing ##### Returns -A subscription instance +The current connection to allow for chaining ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| messageRecieved | [System.Action{MQContract.Messages.RecievedServiceMessage}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Messages.RecievedServiceMessage}') | Callback for when a message is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | Callback for when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to bind to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The group to subscribe as part of | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The service channel options, if desired, specifically the StoredEventsSubscriptionOptions which is used to access stored event streams | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when options is not null and is not an instance of the type StoredEventsSubscriptionOptions | - - -### SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` - -##### Summary - -Called to create a subscription for queries to the underlying KubeMQ server - -##### Returns - -A subscription instance - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}') | Callback for when a query is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | Callback for when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to bind to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The group to subscribe as part of | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Should be null here as there is no Service Channel Options implemented for this call | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown if options was supplied because there are no implemented options for this call | +| channelName | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel | +| readStyle | [MQContract.KubeMQ.Connection.MessageReadStyle](#T-MQContract-KubeMQ-Connection-MessageReadStyle 'MQContract.KubeMQ.Connection.MessageReadStyle') | Set the message reading style when subscribing | +| readOffset | [System.Int64](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Int64 'System.Int64') | Set the readoffset to use for the given reading style | ## ConnectionOptions `type` @@ -754,53 +626,53 @@ Holder for reflection information generated from SDK/Grpc/kubemq.proto File descriptor for SDK/Grpc/kubemq.proto - + ## MessageReadStyle `type` ##### Namespace -MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions +MQContract.KubeMQ.Connection ##### Summary These are the different read styles to use when subscribing to a stored Event PubSub - + ### StartAtSequence `constants` ##### Summary Start at message number X (this value is specified when creating the listener) - + ### StartAtTime `constants` ##### Summary Start at time X (this value is specified when creating the listener) - + ### StartAtTimeDelta `constants` ##### Summary Start at Time Delte X (this value is specified when creating the listener) - + ### StartFromFirst `constants` ##### Summary Start at the beginning - + ### StartFromLast `constants` ##### Summary Start at the last message - + ### StartNewOnly `constants` ##### Summary @@ -972,43 +844,6 @@ Field number for the "StreamRequestTypeData" field. Field number for the "TransactionId" field. - -## PublishChannelOptions `type` - -##### Namespace - -MQContract.KubeMQ.Options - -##### Summary - -Houses the Publish Channel options used when calling the Publish command - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| Stored | [T:MQContract.KubeMQ.Options.PublishChannelOptions](#T-T-MQContract-KubeMQ-Options-PublishChannelOptions 'T:MQContract.KubeMQ.Options.PublishChannelOptions') | Indicates if the publish should be using storage | - - -### #ctor(Stored) `constructor` - -##### Summary - -Houses the Publish Channel options used when calling the Publish command - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| Stored | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Indicates if the publish should be using storage | - - -### Stored `property` - -##### Summary - -Indicates if the publish should be using storage - ## QueueMessage `type` @@ -1576,51 +1411,6 @@ Field number for the "MessageID" field. Field number for the "SentAt" field. - -## StoredEventsSubscriptionOptions `type` - -##### Namespace - -MQContract.KubeMQ.Options - -##### Summary - -Houses the configuration for a subscription going to a stored message channel - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| ReadStyle | [T:MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions](#T-T-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions 'T:MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions') | The read style to use | - - -### #ctor(ReadStyle,ReadOffset) `constructor` - -##### Summary - -Houses the configuration for a subscription going to a stored message channel - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| ReadStyle | [MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle](#T-MQContract-KubeMQ-Options-StoredEventsSubscriptionOptions-MessageReadStyle 'MQContract.KubeMQ.Options.StoredEventsSubscriptionOptions.MessageReadStyle') | The read style to use | -| ReadOffset | [System.Int64](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Int64 'System.Int64') | The read offset to use | - - -### ReadOffset `property` - -##### Summary - -The read offset to use - - -### ReadStyle `property` - -##### Summary - -The read style to use - ## StreamQueueMessagesRequest `type` diff --git a/Connectors/KubeMQ/SDK/KubeClient.cs b/Connectors/KubeMQ/SDK/KubeClient.cs index ce90fdb..55809e6 100644 --- a/Connectors/KubeMQ/SDK/KubeClient.cs +++ b/Connectors/KubeMQ/SDK/KubeClient.cs @@ -6,7 +6,7 @@ namespace MQContract.KubeMQ.SDK.Connection { - internal class KubeClient : IDisposable + internal class KubeClient : IAsyncDisposable { private const int RETRY_COUNT = 5; @@ -163,31 +163,21 @@ internal AsyncServerStreamingCall SubscribeToEvents(Subscribe subs return client.SubscribeToEvents(subscribe, headers: headers, cancellationToken: cancellationToken); }); - protected virtual void Dispose(bool disposing) + public async ValueTask DisposeAsync() { if (!disposedValue) { - if (disposing) + disposedValue=true; + try { - try - { - channel.ShutdownAsync().Wait(); - } - catch (Exception ex) - { - logger?.LogError(ex,"Error shutting down grpc Kube Channel: {ErrorMessage}",ex.Message); - } - channel.Dispose(); + await channel.ShutdownAsync(); } - disposedValue=true; + catch (Exception ex) + { + logger?.LogError(ex, "Error shutting down grpc Kube Channel: {ErrorMessage}", ex.Message); + } + channel.Dispose(); } } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/Connectors/KubeMQ/Subscriptions/PubSubscription.cs b/Connectors/KubeMQ/Subscriptions/PubSubscription.cs index 3e16391..ebeb248 100644 --- a/Connectors/KubeMQ/Subscriptions/PubSubscription.cs +++ b/Connectors/KubeMQ/Subscriptions/PubSubscription.cs @@ -9,9 +9,9 @@ namespace MQContract.KubeMQ.Subscriptions { internal class PubSubscription(ConnectionOptions options, KubeClient client, - Action messageRecieved, Action errorRecieved, string channel, string group, - StoredEventsSubscriptionOptions? storageOptions, CancellationToken cancellationToken) : - SubscriptionBase(options.Logger,options.ReconnectInterval,client,errorRecieved,cancellationToken) + Action messageReceived, Action errorReceived, string channel, string group, + StoredChannelOptions? storageOptions, CancellationToken cancellationToken) : + SubscriptionBase(options.Logger,options.ReconnectInterval,client,errorReceived,cancellationToken) { private readonly KubeClient Client = client; @@ -31,10 +31,10 @@ protected override AsyncServerStreamingCall EstablishCall() cancelToken.Token); } - protected override Task MessageRecieved(EventReceive message) + protected override ValueTask MessageReceived(EventReceive message) { - messageRecieved(new(message.EventID,message.Metadata,message.Channel,Connection.ConvertMessageHeader(message.Tags),message.Body.ToArray())); - return Task.CompletedTask; + messageReceived(new(message.EventID,message.Metadata,message.Channel,Connection.ConvertMessageHeader(message.Tags),message.Body.ToArray())); + return ValueTask.CompletedTask; } } } diff --git a/Connectors/KubeMQ/Subscriptions/QuerySubscription.cs b/Connectors/KubeMQ/Subscriptions/QuerySubscription.cs index e42f9ba..e098d33 100644 --- a/Connectors/KubeMQ/Subscriptions/QuerySubscription.cs +++ b/Connectors/KubeMQ/Subscriptions/QuerySubscription.cs @@ -7,9 +7,9 @@ namespace MQContract.KubeMQ.Subscriptions { internal class QuerySubscription(ConnectionOptions options, KubeClient client, - Func> messageRecieved, Action errorRecieved, + Func> messageReceived, Action errorReceived, string channel, string group, CancellationToken cancellationToken) - : SubscriptionBase(options.Logger,options.ReconnectInterval,client,errorRecieved,cancellationToken) + : SubscriptionBase(options.Logger,options.ReconnectInterval,client,errorReceived,cancellationToken) { private readonly KubeClient Client = client; protected override AsyncServerStreamingCall EstablishCall() @@ -26,12 +26,12 @@ protected override AsyncServerStreamingCall EstablishCall() cancelToken.Token); } - protected override async Task MessageRecieved(Request message) + protected override async ValueTask MessageReceived(Request message) { ServiceMessage? result; try { - result = await messageRecieved(new(message.RequestID,message.Metadata,message.Channel,Connection.ConvertMessageHeader(message.Tags),message.Body.ToArray())); + result = await messageReceived(new(message.RequestID,message.Metadata,message.Channel,Connection.ConvertMessageHeader(message.Tags),message.Body.ToArray())); } catch (Exception ex) { diff --git a/Connectors/KubeMQ/Subscriptions/SubscriptionBase.cs b/Connectors/KubeMQ/Subscriptions/SubscriptionBase.cs index 135d53d..180a3ad 100644 --- a/Connectors/KubeMQ/Subscriptions/SubscriptionBase.cs +++ b/Connectors/KubeMQ/Subscriptions/SubscriptionBase.cs @@ -6,7 +6,7 @@ namespace MQContract.KubeMQ.Subscriptions { internal abstract class SubscriptionBase(ILogger? logger,int reconnectInterval, KubeClient client, - Action errorRecieved, CancellationToken cancellationToken) : IServiceSubscription + Action errorReceived, CancellationToken cancellationToken) : IServiceSubscription where T : class { private bool disposedValue; @@ -15,7 +15,7 @@ internal abstract class SubscriptionBase(ILogger? logger,int reconnectInterva protected readonly CancellationTokenSource cancelToken = new(); protected abstract AsyncServerStreamingCall EstablishCall(); - protected abstract Task MessageRecieved(T message); + protected abstract ValueTask MessageReceived(T message); public void Run() { @@ -24,10 +24,10 @@ public void Run() cancelToken.Cancel(); }); - cancelToken.Token.Register(() => + cancelToken.Token.Register(async () => { active = false; - client.Dispose(); + await client.DisposeAsync(); }); Task.Run(async () => { @@ -40,7 +40,7 @@ public void Run() await foreach (var resp in call.ResponseStream.ReadAllAsync(cancelToken.Token)) { if (active) - await MessageRecieved(resp); + await MessageReceived(resp); else break; } @@ -54,25 +54,25 @@ public void Run() case StatusCode.Cancelled: case StatusCode.PermissionDenied: case StatusCode.Aborted: - EndAsync().Wait(); + await EndAsync(); break; case StatusCode.Unknown: case StatusCode.Unavailable: case StatusCode.DataLoss: case StatusCode.DeadlineExceeded: - logger?.LogTrace("RPC Error recieved on subscription {SubscriptionID}, retrying connection after delay {ReconnectDelay}ms. StatusCode:{StatusCode},Message:{ErrorMessage}", ID, reconnectInterval, rpcx.StatusCode, rpcx.Message); + logger?.LogTrace("RPC Error received on subscription {SubscriptionID}, retrying connection after delay {ReconnectDelay}ms. StatusCode:{StatusCode},Message:{ErrorMessage}", ID, reconnectInterval, rpcx.StatusCode, rpcx.Message); break; default: - logger?.LogError(rpcx, "RPC Error recieved on subscription {SubscriptionID}. StatusCode:{StatusCode},Message:{ErrorMessage}", ID, rpcx.StatusCode, rpcx.Message); - errorRecieved(rpcx); + logger?.LogError(rpcx, "RPC Error received on subscription {SubscriptionID}. StatusCode:{StatusCode},Message:{ErrorMessage}", ID, rpcx.StatusCode, rpcx.Message); + errorReceived(rpcx); break; } } } catch (Exception e) { - logger?.LogError(e, "Error recieved on subscription {SubscriptionID}. Message:{ErrorMessage}", ID, e.Message); - errorRecieved(e); + logger?.LogError(e, "Error received on subscription {SubscriptionID}. Message:{ErrorMessage}", ID, e.Message); + errorReceived(e); } if (active && !cancellationToken.IsCancellationRequested) await Task.Delay(reconnectInterval); @@ -80,7 +80,7 @@ public void Run() }); } - public async Task EndAsync() + public async ValueTask EndAsync() { if (active) { @@ -88,28 +88,20 @@ public async Task EndAsync() try { await cancelToken.CancelAsync(); - client.Dispose(); + await client.DisposeAsync(); cancelToken.Dispose(); } catch{ } } } - protected virtual void Dispose(bool disposing) + public async ValueTask DisposeAsync() { - if (!disposedValue) + if (!disposedValue && active) { - if (disposing && active) - EndAsync().Wait(); - disposedValue = true; + disposedValue=true; + await EndAsync(); } } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/Connectors/NATS/Connection.cs b/Connectors/NATS/Connection.cs index b9565bd..c0a9e03 100644 --- a/Connectors/NATS/Connection.cs +++ b/Connectors/NATS/Connection.cs @@ -13,7 +13,7 @@ namespace MQContract.NATS /// /// This is the MessageServiceConnection implementation for using NATS.io /// - public class Connection : IMessageServiceConnection + public sealed class Connection : IQueryResponseMessageServiceConnection,IPingableMessageServiceConnection, IAsyncDisposable,IDisposable { private const string MESSAGE_IDENTIFIER_HEADER = "_MessageID"; private const string MESSAGE_TYPE_HEADER = "_MessageTypeID"; @@ -21,9 +21,8 @@ public class Connection : IMessageServiceConnection private readonly NatsConnection natsConnection; private readonly NatsJSContext natsJSContext; + private readonly List subscriptionConsumerConfigs = []; private readonly ILogger? logger; - private readonly List subscriptions = []; - private readonly SemaphoreSlim dataLock = new(1, 1); private bool disposedValue; /// @@ -58,13 +57,13 @@ private async Task ProcessConnection() /// The maximum message body size allowed. /// DEFAULT: 1MB /// - public int? MaxMessageBodySize { get; init; } = 1024*1024*1; //1MB default + public uint? MaxMessageBodySize { get; init; } = 1024*1024*1; //1MB default /// /// The default timeout to use for RPC calls when not specified by class or in the call. /// DEFAULT: 30 seconds /// - public TimeSpan DefaultTimout { get; init; } = TimeSpan.FromSeconds(30); + public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromMinutes(1); /// /// Called to define a Stream inside the underlying NATS context. This is an exposure of the NatsJSContext.CreateStreamAsync @@ -76,10 +75,19 @@ public ValueTask CreateStreamAsync(StreamConfig streamConfig,Canc => natsJSContext.CreateStreamAsync(streamConfig, cancellationToken); /// - /// Called to ping the NATS.io service + /// Called to register a consumer configuration for a given channel. This is only used for stream channels and allows for configuring + /// storing and reading patterns /// - /// The Ping Result including service information - public async Task PingAsync() + /// The underlying stream name that this configuration applies to + /// The consumer configuration to use for that stream + /// The underlying connection to allow for chaining + public Connection RegisterConsumerConfig(string channelName, ConsumerConfig consumerConfig) + { + subscriptionConsumerConfigs.Add(new(channelName, consumerConfig)); + return this; + } + + async ValueTask IPingableMessageServiceConnection.PingAsync() => new PingResult(natsConnection.ServerInfo?.Host??string.Empty, natsConnection.ServerInfo?.Version??string.Empty, await natsConnection.PingAsync() @@ -123,60 +131,26 @@ internal static NatsHeaders ProduceQueryError(Exception exception,string message ])); } - /// - /// Called to publish a message into the NATS io server - /// - /// The service message being sent - /// The service channel options, if desired, specifically the StreamPublishChannelOptions which is used to access streams vs standard publish method - /// A cancellation token - /// Transmition result identifying if it worked or not - /// Thrown when an attempt to pass an options object that is not of the type StreamPublishChannelOptions - public async Task PublishAsync(ServiceMessage message, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + async ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) { - InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(options); try { - if (options is StreamPublishChannelOptions publishChannelOptions) - { - if (publishChannelOptions.Config!=null) - await CreateStreamAsync(publishChannelOptions.Config, cancellationToken); - var ack = await natsJSContext.PublishAsync( + await natsConnection.PublishAsync( message.Channel, message.Data.ToArray(), headers: ExtractHeader(message), cancellationToken: cancellationToken ); - return new TransmissionResult(message.ID, (ack.Error!=null ? $"{ack.Error.Code}:{ack.Error.Description}" : null)); - } - else - { - await natsConnection.PublishAsync( - message.Channel, - message.Data.ToArray(), - headers: ExtractHeader(message), - cancellationToken: cancellationToken - ); - return new TransmissionResult(message.ID); - } - }catch(Exception ex) + return new TransmissionResult(message.ID); + } + catch(Exception ex) { return new TransmissionResult(message.ID, ex.Message); } } - /// - /// Called to publish a query into the NATS io server - /// - /// The service message being sent - /// The timeout supplied for the query to response - /// Should be null here as there is no Service Channel Options implemented for this call - /// A cancellation token - /// The resulting response - /// Thrown if options was supplied because there are no implemented options for this call - /// Thrown when an error comes from the responding service - public async Task QueryAsync(ServiceMessage message, TimeSpan timeout, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + async ValueTask IQueryResponseMessageServiceConnection.QueryAsync(ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) { - NoChannelOptionsAvailableException.ThrowIfNotNull(options); var result = await natsConnection.RequestAsync( message.Channel, message.Data.ToArray(), @@ -195,27 +169,30 @@ public async Task QueryAsync(ServiceMessage message, TimeSpa ); } - /// - /// Called to create a subscription to the underlying nats server - /// - /// Callback for when a message is recieved - /// Callback for when an error occurs - /// The name of the channel to bind to - /// The queueGroup to use for the subscription - /// The service channel options, if desired, specifically the StreamPublishSubscriberOptions which is used to access streams vs standard subscription - /// A cancellation token - /// A subscription instance - /// Thrown when options is not null and is not an instance of the type StreamPublishSubscriberOptions - public async Task SubscribeAsync(Action messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + async ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) { - InvalidChannelOptionsTypeException.ThrowIfNotNullAndNotOfType(options); - IInternalServiceSubscription? subscription = null; - if (options is StreamPublishSubscriberOptions subscriberOptions) + SubscriptionBase subscription; + var isStream = false; +#pragma warning disable S3267 // Loops should be simplified with "LINQ" expressions + await foreach(var name in natsJSContext.ListStreamNamesAsync(cancellationToken: cancellationToken)) + { + if (Equals(channel, name)) + { + isStream=true; + break; + } + } +#pragma warning restore S3267 // Loops should be simplified with "LINQ" expressions + if (isStream) { - if (subscriberOptions.StreamConfig!=null) - await CreateStreamAsync(subscriberOptions.StreamConfig, cancellationToken); - var consumer = await natsJSContext.CreateOrUpdateConsumerAsync(subscriberOptions.StreamConfig?.Name??channel, subscriberOptions.ConsumerConfig??new ConsumerConfig(group) { AckPolicy = ConsumerConfigAckPolicy.Explicit }, cancellationToken); - subscription = new StreamSubscription(consumer, messageRecieved, errorRecieved, cancellationToken); + var config = subscriptionConsumerConfigs.Find(scc => Equals(scc.Channel, channel) + &&( + (group==null && string.IsNullOrWhiteSpace(scc.Configuration.Name) && string.IsNullOrWhiteSpace(scc.Configuration.DurableName)) + ||Equals(group, scc.Configuration.Name) + ||Equals(group, scc.Configuration.DurableName) + )); + var consumer = await natsJSContext.CreateOrUpdateConsumerAsync(channel, config?.Configuration??new ConsumerConfig(group??Guid.NewGuid().ToString()) { AckPolicy = ConsumerConfigAckPolicy.Explicit }, cancellationToken); + subscription = new StreamSubscription(consumer, messageReceived, errorReceived); } else subscription = new PublishSubscription( @@ -224,74 +201,50 @@ public async Task QueryAsync(ServiceMessage message, TimeSpa queueGroup: group, cancellationToken: cancellationToken ), - messageRecieved, - errorRecieved, - cancellationToken + messageReceived, + errorReceived ); subscription.Run(); - await dataLock.WaitAsync(cancellationToken); - subscriptions.Add(subscription); - dataLock.Release(); return subscription; } - /// - /// Called to create a subscription for queries to the underlying NATS server - /// - /// Callback for when a query is recieved - /// Callback for when an error occurs - /// The name of the channel to bind to - /// The queueGroup to use for the subscription - /// Should be null here as there is no Service Channel Options implemented for this call - /// A cancellation token - /// A subscription instance - /// /// Thrown if options was supplied because there are no implemented options for this call - public async Task SubscribeQueryAsync(Func> messageRecieved, Action errorRecieved, string channel, string group, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) + ValueTask IQueryableMessageServiceConnection.SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) { - NoChannelOptionsAvailableException.ThrowIfNotNull(options); var sub = new QuerySubscription( natsConnection.SubscribeAsync( channel, queueGroup: group, cancellationToken: cancellationToken ), - messageRecieved, - errorRecieved, - cancellationToken + messageReceived, + errorReceived ); sub.Run(); - await dataLock.WaitAsync(cancellationToken); - subscriptions.Add(sub); - dataLock.Release(); - return sub; + return ValueTask.FromResult(sub); } + + ValueTask IMessageServiceConnection.CloseAsync() + => natsConnection.DisposeAsync(); - /// - /// Called to dispose of the resources used - /// - /// Indicates if it is disposing - protected virtual void Dispose(bool disposing) + async ValueTask IAsyncDisposable.DisposeAsync() + { + await natsConnection.DisposeAsync().ConfigureAwait(true); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) { if (!disposedValue) { if (disposing) - { - dataLock.Wait(); - foreach (var sub in subscriptions) - sub.EndAsync().Wait(); - subscriptions.Clear(); - Task.Run(async () => await natsConnection.DisposeAsync()).Wait(); - dataLock.Release(); - dataLock.Dispose(); - } + natsConnection.DisposeAsync().AsTask().Wait(); disposedValue=true; } } - - /// - /// Called to dispose of the resources used - /// - public void Dispose() + + void IDisposable.Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); diff --git a/Connectors/NATS/NATS.csproj b/Connectors/NATS/NATS.csproj index 40022d1..fd15ba4 100644 --- a/Connectors/NATS/NATS.csproj +++ b/Connectors/NATS/NATS.csproj @@ -1,25 +1,14 @@  + + - net8.0 - enable - enable - MQContract.$(MSBuildProjectName) - MQContract.$(MSBuildProjectName) - true - $(MSBuildProjectDirectory)\Readme.md - roger-castaldo + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) NATS.io Connector for MQContract - $(AssemblyName) - https://github.com/roger-castaldo/MQContract - Readme.md - https://github.com/roger-castaldo/MQContract - Message Queue MQ Contract NATS - MIT - True - True - True - snupkg @@ -27,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Connectors/NATS/Options/StreamPublishChannelOptions.cs b/Connectors/NATS/Options/StreamPublishChannelOptions.cs deleted file mode 100644 index da3e13d..0000000 --- a/Connectors/NATS/Options/StreamPublishChannelOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MQContract.Interfaces.Service; -using NATS.Client.JetStream.Models; - -namespace MQContract.NATS.Options -{ - /// - /// Used to specify when a publish call is publishing to a JetStream - /// - /// The StreamConfig to use if not already defined - public record StreamPublishChannelOptions(StreamConfig? Config=null) : IServiceChannelOptions - { - } -} diff --git a/Connectors/NATS/Options/StreamPublishSubscriberOptions.cs b/Connectors/NATS/Options/StreamPublishSubscriberOptions.cs deleted file mode 100644 index e07e6f2..0000000 --- a/Connectors/NATS/Options/StreamPublishSubscriberOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MQContract.Interfaces.Service; -using NATS.Client.JetStream.Models; - -namespace MQContract.NATS.Options -{ - /// - /// Used to specify when a subscription call is subscribing to a JetStream and not the standard subscription - /// - /// The StreamConfig to use if not already defined - /// The ConsumerCondig to use if specific settings are required - public record StreamPublishSubscriberOptions(StreamConfig? StreamConfig=null,ConsumerConfig? ConsumerConfig=null) : IServiceChannelOptions - { - } -} diff --git a/Connectors/NATS/Options/SubscriptionConsumerConfig.cs b/Connectors/NATS/Options/SubscriptionConsumerConfig.cs new file mode 100644 index 0000000..2ff1851 --- /dev/null +++ b/Connectors/NATS/Options/SubscriptionConsumerConfig.cs @@ -0,0 +1,8 @@ +using NATS.Client.JetStream.Models; + +namespace MQContract.NATS.Options +{ + internal record SubscriptionConsumerConfig(string Channel,ConsumerConfig Configuration) + { + } +} diff --git a/Connectors/NATS/Readme.md b/Connectors/NATS/Readme.md index d9ede67..6fd62dc 100644 --- a/Connectors/NATS/Readme.md +++ b/Connectors/NATS/Readme.md @@ -5,23 +5,10 @@ - [Connection](#T-MQContract-NATS-Connection 'MQContract.NATS.Connection') - [#ctor(options)](#M-MQContract-NATS-Connection-#ctor-NATS-Client-Core-NatsOpts- 'MQContract.NATS.Connection.#ctor(NATS.Client.Core.NatsOpts)') - - [DefaultTimout](#P-MQContract-NATS-Connection-DefaultTimout 'MQContract.NATS.Connection.DefaultTimout') + - [DefaultTimeout](#P-MQContract-NATS-Connection-DefaultTimeout 'MQContract.NATS.Connection.DefaultTimeout') - [MaxMessageBodySize](#P-MQContract-NATS-Connection-MaxMessageBodySize 'MQContract.NATS.Connection.MaxMessageBodySize') - [CreateStreamAsync(streamConfig,cancellationToken)](#M-MQContract-NATS-Connection-CreateStreamAsync-NATS-Client-JetStream-Models-StreamConfig,System-Threading-CancellationToken- 'MQContract.NATS.Connection.CreateStreamAsync(NATS.Client.JetStream.Models.StreamConfig,System.Threading.CancellationToken)') - - [Dispose(disposing)](#M-MQContract-NATS-Connection-Dispose-System-Boolean- 'MQContract.NATS.Connection.Dispose(System.Boolean)') - - [Dispose()](#M-MQContract-NATS-Connection-Dispose 'MQContract.NATS.Connection.Dispose') - - [PingAsync()](#M-MQContract-NATS-Connection-PingAsync 'MQContract.NATS.Connection.PingAsync') - - [PublishAsync(message,options,cancellationToken)](#M-MQContract-NATS-Connection-PublishAsync-MQContract-Messages-ServiceMessage,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.NATS.Connection.PublishAsync(MQContract.Messages.ServiceMessage,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync(message,timeout,options,cancellationToken)](#M-MQContract-NATS-Connection-QueryAsync-MQContract-Messages-ServiceMessage,System-TimeSpan,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.NATS.Connection.QueryAsync(MQContract.Messages.ServiceMessage,System.TimeSpan,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-NATS-Connection-SubscribeAsync-System-Action{MQContract-Messages-RecievedServiceMessage},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.NATS.Connection.SubscribeAsync(System.Action{MQContract.Messages.RecievedServiceMessage},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken)](#M-MQContract-NATS-Connection-SubscribeQueryAsync-System-Func{MQContract-Messages-RecievedServiceMessage,System-Threading-Tasks-Task{MQContract-Messages-ServiceMessage}},System-Action{System-Exception},System-String,System-String,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.NATS.Connection.SubscribeQueryAsync(System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}},System.Action{System.Exception},System.String,System.String,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') -- [StreamPublishChannelOptions](#T-MQContract-NATS-Options-StreamPublishChannelOptions 'MQContract.NATS.Options.StreamPublishChannelOptions') - - [#ctor(Config)](#M-MQContract-NATS-Options-StreamPublishChannelOptions-#ctor-NATS-Client-JetStream-Models-StreamConfig- 'MQContract.NATS.Options.StreamPublishChannelOptions.#ctor(NATS.Client.JetStream.Models.StreamConfig)') - - [Config](#P-MQContract-NATS-Options-StreamPublishChannelOptions-Config 'MQContract.NATS.Options.StreamPublishChannelOptions.Config') -- [StreamPublishSubscriberOptions](#T-MQContract-NATS-Options-StreamPublishSubscriberOptions 'MQContract.NATS.Options.StreamPublishSubscriberOptions') - - [#ctor(StreamConfig,ConsumerConfig)](#M-MQContract-NATS-Options-StreamPublishSubscriberOptions-#ctor-NATS-Client-JetStream-Models-StreamConfig,NATS-Client-JetStream-Models-ConsumerConfig- 'MQContract.NATS.Options.StreamPublishSubscriberOptions.#ctor(NATS.Client.JetStream.Models.StreamConfig,NATS.Client.JetStream.Models.ConsumerConfig)') - - [ConsumerConfig](#P-MQContract-NATS-Options-StreamPublishSubscriberOptions-ConsumerConfig 'MQContract.NATS.Options.StreamPublishSubscriberOptions.ConsumerConfig') - - [StreamConfig](#P-MQContract-NATS-Options-StreamPublishSubscriberOptions-StreamConfig 'MQContract.NATS.Options.StreamPublishSubscriberOptions.StreamConfig') + - [RegisterConsumerConfig(channelName,consumerConfig)](#M-MQContract-NATS-Connection-RegisterConsumerConfig-System-String,NATS-Client-JetStream-Models-ConsumerConfig- 'MQContract.NATS.Connection.RegisterConsumerConfig(System.String,NATS.Client.JetStream.Models.ConsumerConfig)') - [UnableToConnectException](#T-MQContract-NATS-UnableToConnectException 'MQContract.NATS.UnableToConnectException') @@ -48,8 +35,8 @@ Primary constructor to create an instance using the supplied configuration optio | ---- | ---- | ----------- | | options | [NATS.Client.Core.NatsOpts](#T-NATS-Client-Core-NatsOpts 'NATS.Client.Core.NatsOpts') | | - -### DefaultTimout `property` + +### DefaultTimeout `property` ##### Summary @@ -82,234 +69,24 @@ The stream creation result | streamConfig | [NATS.Client.JetStream.Models.StreamConfig](#T-NATS-Client-JetStream-Models-StreamConfig 'NATS.Client.JetStream.Models.StreamConfig') | The configuration settings for the stream | | cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -### Dispose(disposing) `method` + +### RegisterConsumerConfig(channelName,consumerConfig) `method` ##### Summary -Called to dispose of the resources used - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| disposing | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Indicates if it is disposing | - - -### Dispose() `method` - -##### Summary - -Called to dispose of the resources used - -##### Parameters - -This method has no parameters. - - -### PingAsync() `method` - -##### Summary - -Called to ping the NATS.io service - -##### Returns - -The Ping Result including service information - -##### Parameters - -This method has no parameters. - - -### PublishAsync(message,options,cancellationToken) `method` - -##### Summary - -Called to publish a message into the NATS io server - -##### Returns - -Transmition result identifying if it worked or not - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message being sent | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The service channel options, if desired, specifically the StreamPublishChannelOptions which is used to access streams vs standard publish method | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when an attempt to pass an options object that is not of the type StreamPublishChannelOptions | - - -### QueryAsync(message,timeout,options,cancellationToken) `method` - -##### Summary - -Called to publish a query into the NATS io server - -##### Returns - -The resulting response - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| message | [MQContract.Messages.ServiceMessage](#T-MQContract-Messages-ServiceMessage 'MQContract.Messages.ServiceMessage') | The service message being sent | -| timeout | [System.TimeSpan](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.TimeSpan 'System.TimeSpan') | The timeout supplied for the query to response | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Should be null here as there is no Service Channel Options implemented for this call | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown if options was supplied because there are no implemented options for this call | -| [MQContract.NATS.QueryAsyncReponseException](#T-MQContract-NATS-QueryAsyncReponseException 'MQContract.NATS.QueryAsyncReponseException') | Thrown when an error comes from the responding service | - - -### SubscribeAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` - -##### Summary - -Called to create a subscription to the underlying nats server - -##### Returns - -A subscription instance - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Action{MQContract.Messages.RecievedServiceMessage}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{MQContract.Messages.RecievedServiceMessage}') | Callback for when a message is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | Callback for when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to bind to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The queueGroup to use for the subscription | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | The service channel options, if desired, specifically the StreamPublishSubscriberOptions which is used to access streams vs standard subscription | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.InvalidChannelOptionsTypeException](#T-MQContract-InvalidChannelOptionsTypeException 'MQContract.InvalidChannelOptionsTypeException') | Thrown when options is not null and is not an instance of the type StreamPublishSubscriberOptions | - - -### SubscribeQueryAsync(messageRecieved,errorRecieved,channel,group,options,cancellationToken) `method` - -##### Summary - -Called to create a subscription for queries to the underlying NATS server +Called to register a consumer configuration for a given channel. This is only used for stream channels and allows for configuring +storing and reading patterns ##### Returns -A subscription instance - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Messages.RecievedServiceMessage,System.Threading.Tasks.Task{MQContract.Messages.ServiceMessage}}') | Callback for when a query is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | Callback for when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to bind to | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The queueGroup to use for the subscription | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | Should be null here as there is no Service Channel Options implemented for this call | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Exceptions - -| Name | Description | -| ---- | ----------- | -| [MQContract.NoChannelOptionsAvailableException](#T-MQContract-NoChannelOptionsAvailableException 'MQContract.NoChannelOptionsAvailableException') | Thrown if options was supplied because there are no implemented options for this call | - - -## StreamPublishChannelOptions `type` - -##### Namespace - -MQContract.NATS.Options - -##### Summary - -Used to specify when a publish call is publishing to a JetStream - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| Config | [T:MQContract.NATS.Options.StreamPublishChannelOptions](#T-T-MQContract-NATS-Options-StreamPublishChannelOptions 'T:MQContract.NATS.Options.StreamPublishChannelOptions') | The StreamConfig to use if not already defined | - - -### #ctor(Config) `constructor` - -##### Summary - -Used to specify when a publish call is publishing to a JetStream - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| Config | [NATS.Client.JetStream.Models.StreamConfig](#T-NATS-Client-JetStream-Models-StreamConfig 'NATS.Client.JetStream.Models.StreamConfig') | The StreamConfig to use if not already defined | - - -### Config `property` - -##### Summary - -The StreamConfig to use if not already defined - - -## StreamPublishSubscriberOptions `type` - -##### Namespace - -MQContract.NATS.Options - -##### Summary - -Used to specify when a subscription call is subscribing to a JetStream and not the standard subscription - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| StreamConfig | [T:MQContract.NATS.Options.StreamPublishSubscriberOptions](#T-T-MQContract-NATS-Options-StreamPublishSubscriberOptions 'T:MQContract.NATS.Options.StreamPublishSubscriberOptions') | The StreamConfig to use if not already defined | - - -### #ctor(StreamConfig,ConsumerConfig) `constructor` - -##### Summary - -Used to specify when a subscription call is subscribing to a JetStream and not the standard subscription +The underlying connection to allow for chaining ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| StreamConfig | [NATS.Client.JetStream.Models.StreamConfig](#T-NATS-Client-JetStream-Models-StreamConfig 'NATS.Client.JetStream.Models.StreamConfig') | The StreamConfig to use if not already defined | -| ConsumerConfig | [NATS.Client.JetStream.Models.ConsumerConfig](#T-NATS-Client-JetStream-Models-ConsumerConfig 'NATS.Client.JetStream.Models.ConsumerConfig') | The ConsumerCondig to use if specific settings are required | - - -### ConsumerConfig `property` - -##### Summary - -The ConsumerCondig to use if specific settings are required - - -### StreamConfig `property` - -##### Summary - -The StreamConfig to use if not already defined +| channelName | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The underlying stream name that this configuration applies to | +| consumerConfig | [NATS.Client.JetStream.Models.ConsumerConfig](#T-NATS-Client-JetStream-Models-ConsumerConfig 'NATS.Client.JetStream.Models.ConsumerConfig') | The consumer configuration to use for that stream | ## UnableToConnectException `type` diff --git a/Connectors/NATS/Subscriptions/PublishSubscription.cs b/Connectors/NATS/Subscriptions/PublishSubscription.cs index 94fce0d..12b595d 100644 --- a/Connectors/NATS/Subscriptions/PublishSubscription.cs +++ b/Connectors/NATS/Subscriptions/PublishSubscription.cs @@ -4,20 +4,20 @@ namespace MQContract.NATS.Subscriptions { internal class PublishSubscription(IAsyncEnumerable> asyncEnumerable, - Action messageRecieved, Action errorRecieved, - CancellationToken cancellationToken) : SubscriptionBase(cancellationToken) + Action messageReceived, Action errorReceived) + : SubscriptionBase() { protected override async Task RunAction() { - await foreach (var msg in asyncEnumerable.WithCancellation(cancelToken.Token)) + await foreach (var msg in asyncEnumerable.WithCancellation(CancelToken)) { try { - messageRecieved(ExtractMessage(msg)); + messageReceived(ExtractMessage(msg)); } catch (Exception ex) { - errorRecieved(ex); + errorReceived(ex); } } } diff --git a/Connectors/NATS/Subscriptions/QuerySubscription.cs b/Connectors/NATS/Subscriptions/QuerySubscription.cs index 217b8ec..de27852 100644 --- a/Connectors/NATS/Subscriptions/QuerySubscription.cs +++ b/Connectors/NATS/Subscriptions/QuerySubscription.cs @@ -4,33 +4,33 @@ namespace MQContract.NATS.Subscriptions { internal class QuerySubscription(IAsyncEnumerable> asyncEnumerable, - Func> messageRecieved, Action errorRecieved, - CancellationToken cancellationToken) : SubscriptionBase(cancellationToken) + Func> messageReceived, Action errorReceived) + : SubscriptionBase() { protected override async Task RunAction() { - await foreach (var msg in asyncEnumerable.WithCancellation(cancelToken.Token)) + await foreach (var msg in asyncEnumerable.WithCancellation(CancelToken)) { - var recievedMessage = ExtractMessage(msg); + var receivedMessage = ExtractMessage(msg); try { - var result = await messageRecieved(recievedMessage); + var result = await messageReceived(receivedMessage); await msg.ReplyAsync( result.Data.ToArray(), headers: Connection.ExtractHeader(result), replyTo: msg.ReplyTo, - cancellationToken: cancelToken.Token + cancellationToken: CancelToken ); } catch (Exception ex) { - errorRecieved(ex); - var headers = Connection.ProduceQueryError(ex, recievedMessage.ID, out var responseData); + errorReceived(ex); + var headers = Connection.ProduceQueryError(ex, receivedMessage.ID, out var responseData); await msg.ReplyAsync( responseData, replyTo: msg.ReplyTo, headers:headers, - cancellationToken: cancelToken.Token + cancellationToken: CancelToken ); } } diff --git a/Connectors/NATS/Subscriptions/StreamSubscription.cs b/Connectors/NATS/Subscriptions/StreamSubscription.cs index 63dcd2f..44fc725 100644 --- a/Connectors/NATS/Subscriptions/StreamSubscription.cs +++ b/Connectors/NATS/Subscriptions/StreamSubscription.cs @@ -3,44 +3,44 @@ namespace MQContract.NATS.Subscriptions { - internal class StreamSubscription(INatsJSConsumer consumer, Action messageRecieved, - Action errorRecieved, CancellationToken cancellationToken) - : SubscriptionBase(cancellationToken) + internal class StreamSubscription(INatsJSConsumer consumer, Action messageReceived, + Action errorReceived) + : SubscriptionBase() { protected override async Task RunAction() { - while (!cancelToken.Token.IsCancellationRequested) + while (!CancelToken.IsCancellationRequested) { try { - await consumer.RefreshAsync(cancelToken.Token); // or try to recreate consumer + await consumer.RefreshAsync(CancelToken); // or try to recreate consumer - await foreach (var msg in consumer.ConsumeAsync().WithCancellation(cancelToken.Token)) + await foreach (var msg in consumer.ConsumeAsync().WithCancellation(CancelToken)) { var success = true; try { - messageRecieved(ExtractMessage(msg)); + messageReceived(ExtractMessage(msg)); } catch (Exception ex) { success=false; - errorRecieved(ex); - await msg.NakAsync(cancellationToken: cancelToken.Token); + errorReceived(ex); + await msg.NakAsync(cancellationToken: CancelToken); } if (success) - await msg.AckAsync(cancellationToken: cancelToken.Token); + await msg.AckAsync(cancellationToken: CancelToken); } } catch (NatsJSProtocolException e) { - errorRecieved(e); + errorReceived(e); } catch (NatsJSException e) { - errorRecieved(e); + errorReceived(e); // log exception - await Task.Delay(1000, cancelToken.Token); // backoff + await Task.Delay(1000, CancelToken); // backoff } } } diff --git a/Connectors/NATS/Subscriptions/SubscriptionBase.cs b/Connectors/NATS/Subscriptions/SubscriptionBase.cs index cb8999f..e7e3d8f 100644 --- a/Connectors/NATS/Subscriptions/SubscriptionBase.cs +++ b/Connectors/NATS/Subscriptions/SubscriptionBase.cs @@ -4,18 +4,20 @@ namespace MQContract.NATS.Subscriptions { - internal abstract class SubscriptionBase(CancellationToken cancellationToken) : IInternalServiceSubscription + internal abstract class SubscriptionBase() : IInternalServiceSubscription,IDisposable { + private readonly CancellationTokenSource CancelTokenSource = new(); private bool disposedValue; - protected readonly CancellationTokenSource cancelToken = new(); - protected static RecievedServiceMessage ExtractMessage(NatsJSMsg recievedMessage) - => ExtractMessage(recievedMessage.Headers, recievedMessage.Subject, recievedMessage.Data); + protected CancellationToken CancelToken => CancelTokenSource.Token; - protected static RecievedServiceMessage ExtractMessage(NatsMsg recievedMessage) - => ExtractMessage(recievedMessage.Headers, recievedMessage.Subject, recievedMessage.Data); + protected static ReceivedServiceMessage ExtractMessage(NatsJSMsg receivedMessage) + => ExtractMessage(receivedMessage.Headers, receivedMessage.Subject, receivedMessage.Data); - private static RecievedServiceMessage ExtractMessage(NatsHeaders? headers, string subject, byte[]? data) + protected static ReceivedServiceMessage ExtractMessage(NatsMsg receivedMessage) + => ExtractMessage(receivedMessage.Headers, receivedMessage.Subject, receivedMessage.Data); + + private static ReceivedServiceMessage ExtractMessage(NatsHeaders? headers, string subject, byte[]? data) { var convertedHeaders = Connection.ExtractHeader(headers, out var messageID, out var messageTypeID); return new( @@ -29,34 +31,33 @@ private static RecievedServiceMessage ExtractMessage(NatsHeaders? headers, strin protected abstract Task RunAction(); public void Run() - { - cancellationToken.Register(() => - { - cancelToken.Cancel(); - }); - RunAction(); - } + => RunAction(); - public async Task EndAsync() + public async ValueTask EndAsync() { - try { await cancelToken.CancelAsync(); } catch { } + if (!CancelTokenSource.IsCancellationRequested) + { + System.Diagnostics.Debug.WriteLine("Calling Cancel Async inside NATS subscription..."); + await CancelTokenSource.CancelAsync(); + System.Diagnostics.Debug.WriteLine("COmpleted Cancel Async inside NATS subscription"); + } } protected virtual void Dispose(bool disposing) { if (!disposedValue) { - if (disposing) - { - cancelToken.Cancel(); - cancelToken.Dispose(); - } + if (disposing&&!CancelTokenSource.IsCancellationRequested) + CancelTokenSource.Cancel(); + + CancelTokenSource.Dispose(); disposedValue=true; } } public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Connectors/RabbitMQ/Connection.cs b/Connectors/RabbitMQ/Connection.cs new file mode 100644 index 0000000..81b637f --- /dev/null +++ b/Connectors/RabbitMQ/Connection.cs @@ -0,0 +1,279 @@ +using MQContract.Interfaces.Service; +using MQContract.Messages; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Text; + +namespace MQContract.RabbitMQ +{ + /// + /// This is the MessageServiceConnection implemenation for using RabbitMQ + /// + public sealed class Connection : IInboxQueryableMessageServiceConnection, IDisposable + { + private const string InboxExchange = "_Inbox"; + + private readonly ConnectionFactory factory; + private readonly IConnection conn; + private readonly IModel channel; + private readonly SemaphoreSlim semaphore = new(1, 1); + private readonly string inboxChannel; + private bool disposedValue; + + /// + /// Default constructor for creating instance + /// + /// The connection factory to use that was built with required authentication and connection information + public Connection(ConnectionFactory factory) + { + this.factory = factory; + if (string.IsNullOrWhiteSpace(this.factory.ClientProvidedName)) + this.factory.ClientProvidedName = Guid.NewGuid().ToString(); + conn = this.factory.CreateConnection(); + channel = conn.CreateModel(); + MaxMessageBodySize = factory.MaxMessageSize; + inboxChannel = $"{InboxExchange}.{factory.ClientProvidedName}"; + } + + /// + /// Used to declare a queue inside the RabbitMQ server + /// + /// The name of the queue + /// Is this queue durable + /// Is this queue exclusive + /// Auto Delete queue when connection closed + /// Additional arguements + /// The connection to allow for chaining calls + public Connection QueueDeclare(string queue, bool durable = false, bool exclusive = false, + bool autoDelete = true, IDictionary? arguments = null) + { + channel.QueueDeclare(queue, durable, exclusive, autoDelete, arguments); + return this; + } + + /// + /// Used to decalre an exchange inside the RabbitMQ server + /// + /// The name of the exchange + /// The type of the exchange + /// Is this durable + /// Auto Delete when connection closed + /// Additional arguements + /// The connection to allow for chaining calls + public Connection ExchangeDeclare(string exchange, string type, bool durable = false, bool autoDelete = false, + IDictionary? arguments = null) + { + channel.ExchangeDeclare(exchange,type,durable,autoDelete,arguments); + return this; + } + + /// + /// Used to delete a queue inside the RabbitMQ server + /// + /// The name of the queue + /// Is unused + /// Is Empty + public void QueueDelete(string queue, bool ifUnused, bool ifEmpty) + => channel.QueueDelete(queue,ifUnused,ifEmpty); + + /// + /// The maximum message body size allowed + /// + public uint? MaxMessageBodySize { get; init; } + + /// + /// The default timeout to use for RPC calls when not specified by class or in the call. + /// DEFAULT: 1 minute + /// + public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromMinutes(1); + + internal static (IBasicProperties props, ReadOnlyMemory) ConvertMessage(ServiceMessage message, IModel channel, Guid? messageId = null) + { + var props = channel.CreateBasicProperties(); + props.MessageId=message.ID; + props.Type = message.MessageTypeID; + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); + if (messageId!=null) + { + bw.Write((byte)1); + bw.Write(messageId.Value.ToByteArray()); + } + else + bw.Write((byte)0); + bw.Write(message.Data.Length); + bw.Write(message.Data.ToArray()); + foreach(var key in message.Header.Keys) + { + var bytes = UTF8Encoding.UTF8.GetBytes(key); + bw.Write(bytes.Length); + bw.Write(bytes); + bytes = UTF8Encoding.UTF8.GetBytes(message.Header[key]!); + bw.Write(bytes.Length); + bw.Write(bytes); + } + bw.Flush(); + return (props, ms.ToArray()); + } + + internal static ReceivedServiceMessage ConvertMessage(BasicDeliverEventArgs eventArgs,string channel, Func acknowledge,out Guid? messageId) + { + using var ms = new MemoryStream(eventArgs.Body.ToArray()); + using var br = new BinaryReader(ms); + var flag = br.ReadByte(); + if (flag==1) + messageId = new Guid(br.ReadBytes(16)); + else + messageId=null; + var data = br.ReadBytes(br.ReadInt32()); + var header = new Dictionary(); + while (br.BaseStream.Position IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) + { + await semaphore.WaitAsync(cancellationToken); + TransmissionResult result; + try + { + (var props, var data) = ConvertMessage(message, this.channel); + channel.BasicPublish(message.Channel,string.Empty,props,data); + result = new TransmissionResult(message.ID); + }catch(Exception e) + { + result = new TransmissionResult(message.ID, e.Message); + } + semaphore.Release(); + return result; + } + + private Subscription ProduceSubscription(IConnection conn, string channel, string? group, Action> messageReceived, Action errorReceived) + { + if (group==null) + { + group = Guid.NewGuid().ToString(); + this.channel.QueueDeclare(queue:group, durable:false, exclusive:false, autoDelete:true); + } + return new Subscription(conn, channel, group,messageReceived,errorReceived); + } + + ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + => ValueTask.FromResult(ProduceSubscription(conn, channel, group, + (@event,modelChannel, acknowledge) => + { + messageReceived(ConvertMessage(@event, channel, acknowledge,out _)); + }, + errorReceived + )); + + ValueTask IInboxQueryableMessageServiceConnection.EstablishInboxSubscriptionAsync(Action messageReceived, CancellationToken cancellationToken) + { + channel.ExchangeDeclare(InboxExchange, ExchangeType.Direct, durable: false, autoDelete: true); + channel.QueueDeclare(inboxChannel, durable: false, exclusive: false, autoDelete: true); + return ValueTask.FromResult(new Subscription( + conn, + InboxExchange, + inboxChannel, + (@event, model, acknowledge) => + { + var responseMessage = ConvertMessage(@event, string.Empty, acknowledge, out var messageId); + if (messageId!=null) + messageReceived(new( + responseMessage.ID, + responseMessage.MessageTypeID, + inboxChannel, + responseMessage.Header, + messageId.Value, + responseMessage.Data, + acknowledge + )); + }, + (error) => { }, + routingKey:inboxChannel + )); + } + + async ValueTask IInboxQueryableMessageServiceConnection.QueryAsync(ServiceMessage message, Guid correlationID, CancellationToken cancellationToken) + { + (var props, var data) = ConvertMessage(message, this.channel, correlationID); + props.ReplyTo = inboxChannel; + await semaphore.WaitAsync(cancellationToken); + TransmissionResult result; + try + { + channel.BasicPublish(message.Channel, string.Empty, props, data); + result = new TransmissionResult(message.ID); + } + catch (Exception e) + { + result = new TransmissionResult(message.ID, e.Message); + } + semaphore.Release(); + return result; + } + + ValueTask IQueryableMessageServiceConnection.SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + => ValueTask.FromResult(ProduceSubscription(conn, channel, group, + async (@event, model, acknowledge) => + { + var result = await messageReceived(ConvertMessage(@event, channel, acknowledge, out var messageID)); + (var props, var data) = ConvertMessage(result, model, messageID); + await semaphore.WaitAsync(cancellationToken); + try + { + this.channel.BasicPublish(InboxExchange, @event.BasicProperties.ReplyTo, props, data); + } + catch (Exception e) + { + errorReceived(e); + } + semaphore.Release(); + }, + errorReceived + )); + + ValueTask IMessageServiceConnection.CloseAsync() + { + Dispose(true); + return ValueTask.CompletedTask; + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + semaphore.Wait(); + channel.Close(); + channel.Dispose(); + conn.Close(); + conn.Dispose(); + semaphore.Release(); + semaphore.Dispose(); + } + disposedValue=true; + } + } + + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Connectors/RabbitMQ/RabbitMQ.csproj b/Connectors/RabbitMQ/RabbitMQ.csproj new file mode 100644 index 0000000..66bd87f --- /dev/null +++ b/Connectors/RabbitMQ/RabbitMQ.csproj @@ -0,0 +1,33 @@ + + + + + + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) + RabbitMQ Connector for MQContract + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + True + \ + + + diff --git a/Connectors/RabbitMQ/Readme.md b/Connectors/RabbitMQ/Readme.md new file mode 100644 index 0000000..244380a --- /dev/null +++ b/Connectors/RabbitMQ/Readme.md @@ -0,0 +1,108 @@ + +# MQContract.RabbitMQ + +## Contents + +- [Connection](#T-MQContract-RabbitMQ-Connection 'MQContract.RabbitMQ.Connection') + - [#ctor(factory)](#M-MQContract-RabbitMQ-Connection-#ctor-RabbitMQ-Client-ConnectionFactory- 'MQContract.RabbitMQ.Connection.#ctor(RabbitMQ.Client.ConnectionFactory)') + - [DefaultTimeout](#P-MQContract-RabbitMQ-Connection-DefaultTimeout 'MQContract.RabbitMQ.Connection.DefaultTimeout') + - [MaxMessageBodySize](#P-MQContract-RabbitMQ-Connection-MaxMessageBodySize 'MQContract.RabbitMQ.Connection.MaxMessageBodySize') + - [ExchangeDeclare(exchange,type,durable,autoDelete,arguments)](#M-MQContract-RabbitMQ-Connection-ExchangeDeclare-System-String,System-String,System-Boolean,System-Boolean,System-Collections-Generic-IDictionary{System-String,System-Object}- 'MQContract.RabbitMQ.Connection.ExchangeDeclare(System.String,System.String,System.Boolean,System.Boolean,System.Collections.Generic.IDictionary{System.String,System.Object})') + - [QueueDeclare(queue,durable,exclusive,autoDelete,arguments)](#M-MQContract-RabbitMQ-Connection-QueueDeclare-System-String,System-Boolean,System-Boolean,System-Boolean,System-Collections-Generic-IDictionary{System-String,System-Object}- 'MQContract.RabbitMQ.Connection.QueueDeclare(System.String,System.Boolean,System.Boolean,System.Boolean,System.Collections.Generic.IDictionary{System.String,System.Object})') + - [QueueDelete(queue,ifUnused,ifEmpty)](#M-MQContract-RabbitMQ-Connection-QueueDelete-System-String,System-Boolean,System-Boolean- 'MQContract.RabbitMQ.Connection.QueueDelete(System.String,System.Boolean,System.Boolean)') + + +## Connection `type` + +##### Namespace + +MQContract.RabbitMQ + +##### Summary + +This is the MessageServiceConnection implemenation for using RabbitMQ + + +### #ctor(factory) `constructor` + +##### Summary + +Default constructor for creating instance + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| factory | [RabbitMQ.Client.ConnectionFactory](#T-RabbitMQ-Client-ConnectionFactory 'RabbitMQ.Client.ConnectionFactory') | The connection factory to use that was built with required authentication and connection information | + + +### DefaultTimeout `property` + +##### Summary + +The default timeout to use for RPC calls when not specified by class or in the call. +DEFAULT: 1 minute + + +### MaxMessageBodySize `property` + +##### Summary + +The maximum message body size allowed + + +### ExchangeDeclare(exchange,type,durable,autoDelete,arguments) `method` + +##### Summary + +Used to decalre an exchange inside the RabbitMQ server + +##### Returns + +The connection to allow for chaining calls + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| exchange | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the exchange | +| type | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The type of the exchange | +| durable | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Is this durable | +| autoDelete | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Auto Delete when connection closed | +| arguments | [System.Collections.Generic.IDictionary{System.String,System.Object}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Collections.Generic.IDictionary 'System.Collections.Generic.IDictionary{System.String,System.Object}') | Additional arguements | + + +### QueueDeclare(queue,durable,exclusive,autoDelete,arguments) `method` + +##### Summary + +Used to declare a queue inside the RabbitMQ server + +##### Returns + +The connection to allow for chaining calls + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| queue | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the queue | +| durable | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Is this queue durable | +| exclusive | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Is this queue exclusive | +| autoDelete | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Auto Delete queue when connection closed | +| arguments | [System.Collections.Generic.IDictionary{System.String,System.Object}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Collections.Generic.IDictionary 'System.Collections.Generic.IDictionary{System.String,System.Object}') | Additional arguements | + + +### QueueDelete(queue,ifUnused,ifEmpty) `method` + +##### Summary + +Used to delete a queue inside the RabbitMQ server + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| queue | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the queue | +| ifUnused | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Is unused | +| ifEmpty | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Is Empty | diff --git a/Connectors/RabbitMQ/Subscription.cs b/Connectors/RabbitMQ/Subscription.cs new file mode 100644 index 0000000..409e592 --- /dev/null +++ b/Connectors/RabbitMQ/Subscription.cs @@ -0,0 +1,42 @@ +using MQContract.Interfaces.Service; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace MQContract.RabbitMQ +{ + internal class Subscription : IServiceSubscription + { + private readonly IModel channel; + private readonly Guid subscriptionID = Guid.NewGuid(); + private readonly string consumerTag; + + public Subscription(IConnection conn,string channel,string group, Action> messageReceived, Action errorReceived,string? routingKey=null) + { + this.channel = conn.CreateModel(); + this.channel.QueueBind(group, channel, routingKey??subscriptionID.ToString()); + this.channel.BasicQos(0, 1, false); + var consumer = new EventingBasicConsumer(this.channel); + consumer.Received+=(sender, @event) => + { + messageReceived( + @event, + this.channel, + () => + { + this.channel.BasicAck(@event.DeliveryTag, false); + return ValueTask.CompletedTask; + } + ); + }; + + consumerTag = this.channel.BasicConsume(group, false, consumer); + } + + public ValueTask EndAsync() + { + channel.BasicCancel(consumerTag); + channel.Close(); + return ValueTask.CompletedTask; + } + } +} diff --git a/Connectors/Redis/Connection.cs b/Connectors/Redis/Connection.cs new file mode 100644 index 0000000..834c533 --- /dev/null +++ b/Connectors/Redis/Connection.cs @@ -0,0 +1,195 @@ +using MQContract.Interfaces.Service; +using MQContract.Messages; +using MQContract.Redis.Subscriptions; +using StackExchange.Redis; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace MQContract.Redis +{ + /// + /// This is the MessageServiceConnection implementation for using Redis + /// + public class Connection : IQueryResponseMessageServiceConnection,IAsyncDisposable,IDisposable + { + private readonly ConnectionMultiplexer connectionMultiplexer; + private readonly IDatabase database; + private readonly Guid connectionID = Guid.NewGuid(); + private bool disposedValue; + + /// + /// Default constructor that requires the Redis Configuration settings to be provided + /// + /// The configuration to use for the redis connections + public Connection(ConfigurationOptions configuration) + { + connectionMultiplexer = ConnectionMultiplexer.Connect(configuration); + database = connectionMultiplexer.GetDatabase(); + } + + /// + /// Called to define a consumer group inside redis for a given channel + /// + /// The name of the channel to use + /// The name of the group to use + /// A ValueTask while the operation executes asynchronously + public async ValueTask DefineConsumerGroupAsync(string channel,string group) + { + if (!(await database.KeyExistsAsync(channel)) || + !(await database.StreamGroupInfoAsync(channel)).Any(x => Equals(x.Name,group))) + { + await database.StreamCreateConsumerGroupAsync(channel, group, "0-0", true); + } + } + + /// + /// The maximum message body size allowed, defaults to 4MB + /// + public uint? MaxMessageBodySize { get; init; } = 1024*1024*4; + + /// + /// The default timeout to allow for a Query Response call to execute, defaults to 1 minute + /// + public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromMinutes(1); + + async ValueTask IMessageServiceConnection.CloseAsync() + => await connectionMultiplexer.CloseAsync(); + + private const string MESSAGE_TYPE_KEY = "_MessageTypeID"; + private const string MESSAGE_ID_KEY = "_MessageID"; + private const string MESSAGE_DATA_KEY = "_MessageData"; + private const string MESSAGE_REPLY_KEY = "_MessageReplyChannel"; + private const string MESSAGE_TIMEOUT_KEY = "_MessageTimeout"; + + internal static NameValueEntry[] ConvertMessage(ServiceMessage message, string? replyChannel=null,TimeSpan? messageTimeout=null) + => message.Header.Keys.Select(k => new NameValueEntry(k, message.Header[k])) + .Concat( + [ + new NameValueEntry(MESSAGE_ID_KEY,message.ID), + new NameValueEntry(MESSAGE_TYPE_KEY,message.MessageTypeID), + new NameValueEntry(MESSAGE_DATA_KEY,message.Data.ToArray()) + ]) + .Concat(replyChannel==null ? [] : [new NameValueEntry(MESSAGE_REPLY_KEY,replyChannel)]) + .Concat(messageTimeout==null ? [] : [new NameValueEntry(MESSAGE_TIMEOUT_KEY,messageTimeout.ToString())] ) + .ToArray(); + + internal static (ReceivedServiceMessage receivedMessage,string? replyChannel,TimeSpan? messageTimeout) ConvertMessage(NameValueEntry[] data, string channel,Func? acknowledge) +#pragma warning disable S6580 // Use a format provider when parsing date and time + => ( + new( + data.First(nve=>Equals(nve.Name,MESSAGE_ID_KEY)).Value.ToString(), + data.First(nve => Equals(nve.Name, MESSAGE_TYPE_KEY)).Value.ToString(), + channel, + new(data.Where(nve=>!Equals(nve.Name,MESSAGE_ID_KEY) + && !Equals(nve.Name,MESSAGE_TYPE_KEY) + && !Equals(nve.Name,MESSAGE_DATA_KEY) + && !Equals(nve.Name,MESSAGE_REPLY_KEY) + && !Equals(nve.Name,MESSAGE_TIMEOUT_KEY) + ) + .Select(nve=>new KeyValuePair(nve.Name!,nve.Value.ToString()))), + (byte[])data.First(nve=>Equals(nve.Name,MESSAGE_DATA_KEY)).Value!, + acknowledge + ), + Array.Find(data,(nve)=>Equals(nve.Name,MESSAGE_REPLY_KEY)).Value.ToString(), + (Array.Exists(data,nve=>Equals(nve.Name,MESSAGE_TIMEOUT_KEY)) ? + TimeSpan.Parse(Array.Find(data, (nve) => Equals(nve.Name, MESSAGE_TIMEOUT_KEY)).Value.ToString()) + : null) + ); +#pragma warning restore S6580 // Use a format provider when parsing date and time + + internal static string EncodeMessage(ServiceMessage result) + => JsonSerializer.Serialize>>( + result.Header.Keys.Select(k=> new KeyValuePair(k, result.Header[k]!)) + .Concat([ + new KeyValuePair(MESSAGE_ID_KEY, result.ID), + new KeyValuePair(MESSAGE_TYPE_KEY,result.MessageTypeID), + new KeyValuePair(MESSAGE_DATA_KEY,result.Data) + ]) + ); + + internal static ServiceQueryResult DecodeMessage(string content) + { + var data = JsonSerializer.Deserialize>>(content)!; + return new( + data.First(pair=>Equals(pair.Key,MESSAGE_ID_KEY)).Value.GetValue(), + new(data.Where(pair=>!Equals(pair.Key,MESSAGE_ID_KEY) && !Equals(pair.Key,MESSAGE_TYPE_KEY) && !Equals(pair.Key,MESSAGE_DATA_KEY)) + .Select(pair=>new KeyValuePair(pair.Key,pair.Value.GetValue())) + ), + data.First(pair => Equals(pair.Key, MESSAGE_TYPE_KEY)).Value.GetValue(), + Convert.FromBase64String(data.First(pair => Equals(pair.Key, MESSAGE_DATA_KEY)).Value.GetValue()) + ); + } + + async ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) + { + try + { + _ = await database.StreamAddAsync(message.Channel, ConvertMessage(message)); + return new(message.ID); + }catch(Exception e) + { + return new(message.ID, e.Message); + } + } + + async ValueTask IMessageServiceConnection.SubscribeAsync(Action messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + { + if (group!=null) + await DefineConsumerGroupAsync(channel, group!); + var result = new PubSubscription(messageReceived, errorReceived, database, connectionID, channel, group); + await result.StartAsync(); + return result; + } + + async ValueTask IQueryResponseMessageServiceConnection.QueryAsync(ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken) + { + var replyID = $"_inbox.{Guid.NewGuid()}"; + await database.StreamAddAsync(message.Channel, ConvertMessage(message, replyID, timeout)); + using var cancellation = new CancellationTokenSource(timeout); + using var cleanupEntry = cancellationToken.Register(() => cancellation.Cancel()); + while (!cancellation.IsCancellationRequested) + { + var keyValue = await database.StringGetDeleteAsync(replyID); + if (!keyValue.IsNull) + return DecodeMessage(keyValue.ToString()); + else + await Task.Delay((int)timeout.TotalMilliseconds/500,cancellationToken); + } + throw new TimeoutException(); + } + + async ValueTask IQueryableMessageServiceConnection.SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) + { + if (group!=null) + await DefineConsumerGroupAsync(channel, group!); + var result = new QueryResponseSubscription(messageReceived, errorReceived, database, connectionID, channel, group); + await result.StartAsync(); + return result; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await connectionMultiplexer.DisposeAsync(); + + Dispose(false); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + connectionMultiplexer.Dispose(); + disposedValue=true; + } + } + + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Connectors/Redis/Readme.md b/Connectors/Redis/Readme.md new file mode 100644 index 0000000..3b1d526 --- /dev/null +++ b/Connectors/Redis/Readme.md @@ -0,0 +1,66 @@ + +# MQContract.Redis + +## Contents + +- [Connection](#T-MQContract-Redis-Connection 'MQContract.Redis.Connection') + - [#ctor(configuration)](#M-MQContract-Redis-Connection-#ctor-StackExchange-Redis-ConfigurationOptions- 'MQContract.Redis.Connection.#ctor(StackExchange.Redis.ConfigurationOptions)') + - [DefaultTimeout](#P-MQContract-Redis-Connection-DefaultTimeout 'MQContract.Redis.Connection.DefaultTimeout') + - [MaxMessageBodySize](#P-MQContract-Redis-Connection-MaxMessageBodySize 'MQContract.Redis.Connection.MaxMessageBodySize') + - [DefineConsumerGroupAsync(channel,group)](#M-MQContract-Redis-Connection-DefineConsumerGroupAsync-System-String,System-String- 'MQContract.Redis.Connection.DefineConsumerGroupAsync(System.String,System.String)') + + +## Connection `type` + +##### Namespace + +MQContract.Redis + +##### Summary + +This is the MessageServiceConnection implementation for using Redis + + +### #ctor(configuration) `constructor` + +##### Summary + +Default constructor that requires the Redis Configuration settings to be provided + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| configuration | [StackExchange.Redis.ConfigurationOptions](#T-StackExchange-Redis-ConfigurationOptions 'StackExchange.Redis.ConfigurationOptions') | The configuration to use for the redis connections | + + +### DefaultTimeout `property` + +##### Summary + +The default timeout to allow for a Query Response call to execute, defaults to 1 minute + + +### MaxMessageBodySize `property` + +##### Summary + +The maximum message body size allowed, defaults to 4MB + + +### DefineConsumerGroupAsync(channel,group) `method` + +##### Summary + +Called to define a consumer group inside redis for a given channel + +##### Returns + +A ValueTask while the operation executes asynchronously + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the channel to use | +| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The name of the group to use | diff --git a/Connectors/Redis/Redis.csproj b/Connectors/Redis/Redis.csproj new file mode 100644 index 0000000..26525c7 --- /dev/null +++ b/Connectors/Redis/Redis.csproj @@ -0,0 +1,33 @@ + + + + + + net8.0 + enable + enable + MQContract.$(MSBuildProjectName) + MQContract.$(MSBuildProjectName) + Redis Connector for MQContract + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + True + \ + + + diff --git a/Connectors/Redis/Subscriptions/PubSubscription.cs b/Connectors/Redis/Subscriptions/PubSubscription.cs new file mode 100644 index 0000000..ea551b7 --- /dev/null +++ b/Connectors/Redis/Subscriptions/PubSubscription.cs @@ -0,0 +1,20 @@ +using MQContract.Messages; +using StackExchange.Redis; + +namespace MQContract.Redis.Subscriptions +{ + internal class PubSubscription(Action messageReceived, Action errorReceived, IDatabase database, Guid connectionID, string channel, string? group) + : SubscriptionBase(errorReceived,database,connectionID,channel,group) + { + protected override ValueTask ProcessMessage(StreamEntry streamEntry, string channel, string? group) + { + (var message,_,_) = Connection.ConvertMessage( + streamEntry.Values, + channel, + () => Acknowledge(streamEntry.Id) + ); + messageReceived(message); + return ValueTask.CompletedTask; + } + } +} diff --git a/Connectors/Redis/Subscriptions/QueryResponseSubscription.cs b/Connectors/Redis/Subscriptions/QueryResponseSubscription.cs new file mode 100644 index 0000000..6cb9294 --- /dev/null +++ b/Connectors/Redis/Subscriptions/QueryResponseSubscription.cs @@ -0,0 +1,21 @@ +using MQContract.Messages; +using StackExchange.Redis; + +namespace MQContract.Redis.Subscriptions +{ + internal class QueryResponseSubscription(Func> messageReceived, Action errorReceived, IDatabase database, Guid connectionID, string channel, string? group) + : SubscriptionBase(errorReceived,database,connectionID,channel,group) + { + protected override async ValueTask ProcessMessage(StreamEntry streamEntry, string channel, string? group) + { + (var message,var responseChannel,var timeout) = Connection.ConvertMessage( + streamEntry.Values, + channel, + ()=> Acknowledge(streamEntry.Id) + ); + var result = await messageReceived(message); + await Database.StreamDeleteAsync(Channel, [streamEntry.Id]); + await Database.StringSetAsync(responseChannel, Connection.EncodeMessage(result), expiry: timeout); + } + } +} diff --git a/Connectors/Redis/Subscriptions/SubscriptionBase.cs b/Connectors/Redis/Subscriptions/SubscriptionBase.cs new file mode 100644 index 0000000..29bd699 --- /dev/null +++ b/Connectors/Redis/Subscriptions/SubscriptionBase.cs @@ -0,0 +1,74 @@ +using MQContract.Interfaces.Service; +using StackExchange.Redis; + +namespace MQContract.Redis.Subscriptions +{ + internal abstract class SubscriptionBase(Action errorReceived, IDatabase database, Guid connectionID, string channel, string? group) : IServiceSubscription,IDisposable + { + private readonly CancellationTokenSource tokenSource = new(); + private bool disposedValue; + + protected CancellationToken Token=>tokenSource.Token; + protected IDatabase Database => database; + protected string Channel => channel; + protected string? Group=> group; + + public Task StartAsync() + { + var resultSource = new TaskCompletionSource(); + RedisValue minId = "-"; + Task.Run(async () => + { + resultSource.TrySetResult(); + while (!Token.IsCancellationRequested) + { + try + { + var result = await (group==null ? database.StreamRangeAsync(channel, minId, "+", 1) : database.StreamReadGroupAsync(channel, group!, connectionID.ToString(), ">", 1)); + if (result.Length!=0) + { + minId = result[0].Id+1; + await ProcessMessage(result[0], channel, group); + } + else + await Task.Delay(50); + } + catch (Exception ex) + { + errorReceived(ex); + } + } + }); + return resultSource.Task; + } + + protected async ValueTask Acknowledge(RedisValue Id) + { + if (Group!=null) + await Database.StreamAcknowledgeAsync(channel, group, Id); + } + + protected abstract ValueTask ProcessMessage(StreamEntry streamEntry,string channel,string? group); + + public async ValueTask EndAsync() + =>await tokenSource.CancelAsync(); + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing && !tokenSource.IsCancellationRequested) + tokenSource.Cancel(); + tokenSource.Dispose(); + disposedValue=true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Core/ChannelMapper.cs b/Core/ChannelMapper.cs index 336f8f8..b871605 100644 --- a/Core/ChannelMapper.cs +++ b/Core/ChannelMapper.cs @@ -10,23 +10,24 @@ internal enum MapTypes Publish, PublishSubscription, Query, - QuerySubscription + QuerySubscription, + QueryResponse } - private sealed record ChannelMap(MapTypes Type,Func IsMatch,Func> Change); + private sealed record ChannelMap(MapTypes Type,Func IsMatch,Func> Change); private readonly List channelMaps = []; private ChannelMapper Append(MapTypes type, string originalChannel, string newChannel) - => Append(type, (key) => Equals(key, originalChannel), (key) => Task.FromResult(newChannel)); + => Append(type, (key) => Equals(key, originalChannel), (key) => ValueTask.FromResult(newChannel)); - private ChannelMapper Append(MapTypes type, string originalChannel, Func> change) + private ChannelMapper Append(MapTypes type, string originalChannel, Func> change) => Append(type, (key) => Equals(key, originalChannel), change); - private ChannelMapper Append(MapTypes type, Func> change) + private ChannelMapper Append(MapTypes type, Func> change) => Append(type, (key) => true, change); - private ChannelMapper Append(MapTypes type,Func isMatch,Func> change) + private ChannelMapper Append(MapTypes type,Func isMatch,Func> change) { channelMaps.Add(new(type, isMatch, change)); return this; @@ -47,7 +48,7 @@ public ChannelMapper AddPublishMap(string originalChannel,string newChannel) /// The original channel that is being used in the connection /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddPublishMap(string originalChannel, Func> mapFunction) + public ChannelMapper AddPublishMap(string originalChannel, Func> mapFunction) => Append(MapTypes.Publish, originalChannel, mapFunction); /// @@ -56,14 +57,14 @@ public ChannelMapper AddPublishMap(string originalChannel, FuncA callback that will return true if the supplied function will mape that channel /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddPublishMap(Func isMatch, Func> mapFunction) + public ChannelMapper AddPublishMap(Func isMatch, Func> mapFunction) => Append(MapTypes.Publish, isMatch, mapFunction); /// /// Add a default map function to call for publish calls /// /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddDefaultPublishMap(Func> mapFunction) + public ChannelMapper AddDefaultPublishMap(Func> mapFunction) => Append(MapTypes.Publish, mapFunction); /// /// Add a direct map for pub/sub subscription calls @@ -79,7 +80,7 @@ public ChannelMapper AddPublishSubscriptionMap(string originalChannel, string ne /// The original channel that is being used in the connection /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddPublishSubscriptionMap(string originalChannel, Func> mapFunction) + public ChannelMapper AddPublishSubscriptionMap(string originalChannel, Func> mapFunction) => Append(MapTypes.PublishSubscription, originalChannel, mapFunction); /// /// Add a map function call pair for pub/sub subscription calls @@ -87,14 +88,14 @@ public ChannelMapper AddPublishSubscriptionMap(string originalChannel, FuncA callback that will return true if the supplied function will mape that channel /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddPublishSubscriptionMap(Func isMatch, Func> mapFunction) + public ChannelMapper AddPublishSubscriptionMap(Func isMatch, Func> mapFunction) => Append(MapTypes.PublishSubscription, isMatch, mapFunction); /// /// Add a default map function to call for pub/sub subscription calls /// /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddDefaultPublishSubscriptionMap(Func> mapFunction) + public ChannelMapper AddDefaultPublishSubscriptionMap(Func> mapFunction) => Append(MapTypes.PublishSubscription, mapFunction); /// /// Add a direct map for query calls @@ -110,7 +111,7 @@ public ChannelMapper AddQueryMap(string originalChannel, string newChannel) /// The original channel that is being used in the connection /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddQueryMap(string originalChannel, Func> mapFunction) + public ChannelMapper AddQueryMap(string originalChannel, Func> mapFunction) => Append(MapTypes.Query, originalChannel, mapFunction); /// /// Add a map function call pair for query calls @@ -118,14 +119,14 @@ public ChannelMapper AddQueryMap(string originalChannel, FuncA callback that will return true if the supplied function will mape that channel /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddQueryMap(Func isMatch, Func> mapFunction) + public ChannelMapper AddQueryMap(Func isMatch, Func> mapFunction) => Append(MapTypes.Query, isMatch, mapFunction); /// /// Add a default map function to call for query calls /// /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddDefaultQueryMap(Func> mapFunction) + public ChannelMapper AddDefaultQueryMap(Func> mapFunction) => Append(MapTypes.Query, mapFunction); /// /// Add a direct map for query/response subscription calls @@ -141,7 +142,7 @@ public ChannelMapper AddQuerySubscriptionMap(string originalChannel, string newC /// The original channel that is being used in the connection /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddQuerySubscriptionMap(string originalChannel, Func> mapFunction) + public ChannelMapper AddQuerySubscriptionMap(string originalChannel, Func> mapFunction) => Append(MapTypes.QuerySubscription, originalChannel, mapFunction); /// /// Add a map function call pair for query/response subscription calls @@ -149,20 +150,52 @@ public ChannelMapper AddQuerySubscriptionMap(string originalChannel, FuncA callback that will return true if the supplied function will mape that channel /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddQuerySubscriptionMap(Func isMatch, Func> mapFunction) + public ChannelMapper AddQuerySubscriptionMap(Func isMatch, Func> mapFunction) => Append(MapTypes.QuerySubscription, isMatch, mapFunction); /// /// Add a default map function to call for query/response subscription calls /// /// A function to be called with the channel supplied expecting a mapped channel name /// The current instance of the Channel Mapper - public ChannelMapper AddDefaultQuerySubscriptionMap(Func> mapFunction) + public ChannelMapper AddDefaultQuerySubscriptionMap(Func> mapFunction) => Append(MapTypes.QuerySubscription, mapFunction); + /// + /// Add a direct map for query/response response calls + /// + /// The original channel that is being used in the connection + /// The channel to map it to + /// The current instance of the Channel Mapper + public ChannelMapper AddQueryResponseMap(string originalChannel, string newChannel) + => Append(MapTypes.QueryResponse, originalChannel, newChannel); + /// + /// Add a map function for query/response response calls + /// + /// The original channel that is being used in the connection + /// A function to be called with the channel supplied expecting a mapped channel name + /// The current instance of the Channel Mapper + public ChannelMapper AddQueryResponseMap(string originalChannel, Func> mapFunction) + => Append(MapTypes.QueryResponse, originalChannel, mapFunction); + /// + /// Add a map function call pair for query/response response calls + /// + /// A callback that will return true if the supplied function will mape that channel + /// A function to be called with the channel supplied expecting a mapped channel name + /// The current instance of the Channel Mapper + public ChannelMapper AddQueryResponseMap(Func isMatch, Func> mapFunction) + => Append(MapTypes.QueryResponse, isMatch, mapFunction); + /// + /// Add a default map function to call for query/response response calls + /// + /// A function to be called with the channel supplied expecting a mapped channel name + /// The current instance of the Channel Mapper + public ChannelMapper AddDefaultQueryResponseMap(Func> mapFunction) + => Append(MapTypes.QueryResponse, mapFunction); - internal Task MapChannel(MapTypes mapType,string originalChannel) + internal async ValueTask MapChannel(MapTypes mapType,string originalChannel) { var map = channelMaps.Find(m=>Equals(m.Type,mapType) && m.IsMatch(originalChannel)); - return map?.Change(originalChannel)??Task.FromResult(originalChannel); + if (map == null) return originalChannel; + return await map.Change(originalChannel); } } } diff --git a/Core/ContractConnection.Metrics.cs b/Core/ContractConnection.Metrics.cs new file mode 100644 index 0000000..0f7f2a0 --- /dev/null +++ b/Core/ContractConnection.Metrics.cs @@ -0,0 +1,40 @@ +using MQContract.Interfaces; +using MQContract.Middleware; +using System.Diagnostics.Metrics; + +namespace MQContract +{ + public sealed partial class ContractConnection + { + IContractConnection IContractConnection.AddMetrics(Meter? meter, bool useInternal) + { + lock (middleware) + { + middleware.Insert(0, new MetricsMiddleware(meter, useInternal)); + } + return this; + } + + private MetricsMiddleware? MetricsMiddleware + { + get + { + MetricsMiddleware? metricsMiddleware; + lock (middleware) + { + metricsMiddleware = middleware.OfType().FirstOrDefault(); + } + return metricsMiddleware; + } + } + + IContractMetric? IContractConnection.GetSnapshot(bool sent) + => MetricsMiddleware?.GetSnapshot(sent); + IContractMetric? IContractConnection.GetSnapshot(Type messageType, bool sent) + => MetricsMiddleware?.GetSnapshot(messageType, sent); + IContractMetric? IContractConnection.GetSnapshot(bool sent) + => MetricsMiddleware?.GetSnapshot(typeof(T), sent); + IContractMetric? IContractConnection.GetSnapshot(string channel, bool sent) + => MetricsMiddleware?.GetSnapshot(channel, sent); + } +} diff --git a/Core/ContractConnection.Middleware.cs b/Core/ContractConnection.Middleware.cs new file mode 100644 index 0000000..fc4a061 --- /dev/null +++ b/Core/ContractConnection.Middleware.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.DependencyInjection; +using MQContract.Interfaces; +using MQContract.Interfaces.Factories; +using MQContract.Interfaces.Middleware; +using MQContract.Messages; +using MQContract.Middleware; + +namespace MQContract +{ + public sealed partial class ContractConnection + { + private ContractConnection RegisterMiddleware(object element) + { + lock (middleware) + { + middleware.Add(element); + } + return this; + } + + private ContractConnection RegisterMiddleware(Type type) + => RegisterMiddleware((serviceProvider == null ? Activator.CreateInstance(type) : ActivatorUtilities.CreateInstance(serviceProvider, type))!); + + IContractConnection IContractConnection.RegisterMiddleware() + => RegisterMiddleware(typeof(T)); + IContractConnection IContractConnection.RegisterMiddleware(Func constructInstance) + => RegisterMiddleware(constructInstance()); + IContractConnection IContractConnection.RegisterMiddleware() + => RegisterMiddleware(typeof(T)); + IContractConnection IContractConnection.RegisterMiddleware(Func constructInstance) + => RegisterMiddleware(constructInstance()); + + private async ValueTask<(T message,string? channel,MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context, T message, string? channel, MessageHeader messageHeader) + where T : class + { + IBeforeEncodeMiddleware[] genericHandlers; + IBeforeEncodeSpecificTypeMiddleware[] specificHandlers; + lock (middleware) + { + genericHandlers = middleware.OfType().ToArray(); + specificHandlers = middleware.OfType>().ToArray(); + } + foreach (var handler in genericHandlers) + (message,channel,messageHeader) = await handler.BeforeMessageEncodeAsync(context,message, channel, messageHeader); + foreach(var handler in specificHandlers) + (message,channel,messageHeader) = await handler.BeforeMessageEncodeAsync(context,message, channel, messageHeader); + return (message, channel, messageHeader); + } + + private async ValueTask AfterMessageEncodeAsync(IContext context,ServiceMessage message) + { + IAfterEncodeMiddleware[] genericHandlers; + lock (middleware) + { + genericHandlers = middleware.OfType().ToArray(); + } + foreach(var handler in genericHandlers) + message = await handler.AfterMessageEncodeAsync(typeof(T),context,message); + return message; + } + + private async ValueTask<(MessageHeader messageHeader, ReadOnlyMemory data)> BeforeMessageDecodeAsync(IContext context, string id, MessageHeader messageHeader, string messageTypeID,string messageChannel, ReadOnlyMemory data) + { + IBeforeDecodeMiddleware[] genericHandlers; + lock (middleware) + { + genericHandlers = middleware.OfType().ToArray(); + } + foreach (var handler in genericHandlers) + (messageHeader,data) = await handler.BeforeMessageDecodeAsync(context,id,messageHeader,messageTypeID,messageChannel,data); + return (messageHeader,data); + } + + private async ValueTask<(T message, MessageHeader messageHeader)> AfterMessageDecodeAsync(IContext context, T message, string ID, MessageHeader messageHeader, DateTime receivedTimestamp, DateTime processedTimeStamp) + where T : class + { + IAfterDecodeMiddleware[] genericHandlers; + IAfterDecodeSpecificTypeMiddleware[] specificHandlers; + lock (middleware) + { + genericHandlers = middleware.OfType().ToArray(); + specificHandlers = middleware.OfType>().ToArray(); + } + foreach (var handler in genericHandlers) + (message, messageHeader) = await handler.AfterMessageDecodeAsync(context, message, ID, messageHeader, receivedTimestamp, processedTimeStamp); + foreach (var handler in specificHandlers) + (message, messageHeader) = await handler.AfterMessageDecodeAsync(context, message, ID, messageHeader, receivedTimestamp, processedTimeStamp); + return (message, messageHeader); + } + + private async ValueTask ProduceServiceMessageAsync(ChannelMapper.MapTypes mapType,IMessageFactory messageFactory, T message, bool ignoreChannel, string? channel = null, MessageHeader? messageHeader = null) + where T : class + { + var context = new Context(mapType); + (message, channel, messageHeader) = await BeforeMessageEncodeAsync(context, message, channel??messageFactory.MessageChannel, messageHeader??new([])); + return await AfterMessageEncodeAsync(context, + await messageFactory.ConvertMessageAsync(message, ignoreChannel, channel, messageHeader) + ); + } + private async ValueTask<(T message,MessageHeader header)> DecodeServiceMessageAsync(ChannelMapper.MapTypes mapType, IMessageFactory messageFactory, ReceivedServiceMessage message) + where T : class + { + var context = new Context(mapType); + (var messageHeader, var data) = await BeforeMessageDecodeAsync(context, message.ID, message.Header, message.MessageTypeID, message.Channel, message.Data); + var taskMessage = await messageFactory.ConvertMessageAsync(logger, new ReceivedServiceMessage(message.ID, message.MessageTypeID, message.Channel, messageHeader, data, message.Acknowledge)) + ??throw new InvalidCastException($"Unable to convert incoming message {message.MessageTypeID} to {typeof(T).FullName}"); + return await AfterMessageDecodeAsync(context, taskMessage!, message.ID, messageHeader, message.ReceivedTimestamp, DateTime.Now); + } + } +} diff --git a/Core/ContractConnection.PubSub.cs b/Core/ContractConnection.PubSub.cs new file mode 100644 index 0000000..f0167aa --- /dev/null +++ b/Core/ContractConnection.PubSub.cs @@ -0,0 +1,47 @@ +using MQContract.Interfaces; +using MQContract.Messages; +using MQContract.Subscriptions; + +namespace MQContract +{ + public partial class ContractConnection + { + private async ValueTask CreateSubscriptionAsync(Func, ValueTask> messageReceived, Action errorReceived, string? channel, string? group, bool ignoreMessageHeader, bool synchronous, CancellationToken cancellationToken) + where T : class + { + var messageFactory = GetMessageFactory(ignoreMessageHeader); + var subscription = new PubSubSubscription( + async (serviceMessage) => + { + (var taskMessage, var messageHeader) = await DecodeServiceMessageAsync(ChannelMapper.MapTypes.PublishSubscription, messageFactory, serviceMessage); + await messageReceived(new ReceivedMessage(serviceMessage.ID, taskMessage!, messageHeader, serviceMessage.ReceivedTimestamp, DateTime.Now)); + }, + errorReceived, + (originalChannel) => MapChannel(ChannelMapper.MapTypes.PublishSubscription, originalChannel)!, + channel: channel, + group: group, + synchronous: synchronous, + logger: logger); + if (await subscription.EstablishSubscriptionAsync(serviceConnection, cancellationToken)) + return subscription; + throw new SubscriptionFailedException(); + } + + async ValueTask IContractConnection.PublishAsync(T message, string? channel, MessageHeader? messageHeader, CancellationToken cancellationToken) + => await serviceConnection.PublishAsync( + await ProduceServiceMessageAsync(ChannelMapper.MapTypes.Publish,GetMessageFactory(),message,false,channel,messageHeader), + cancellationToken + ); + + ValueTask IContractConnection.SubscribeAsync(Func, ValueTask> messageReceived, Action errorReceived, string? channel, string? group, bool ignoreMessageHeader, CancellationToken cancellationToken) where T : class + => CreateSubscriptionAsync(messageReceived, errorReceived, channel, group, ignoreMessageHeader, false, cancellationToken); + + ValueTask IContractConnection.SubscribeAsync(Action> messageReceived, Action errorReceived, string? channel, string? group, bool ignoreMessageHeader, CancellationToken cancellationToken) where T : class + => CreateSubscriptionAsync((msg) => + { + messageReceived(msg); + return ValueTask.CompletedTask; + }, + errorReceived, channel, group, ignoreMessageHeader, true, cancellationToken); + } +} diff --git a/Core/ContractConnection.QueryResponse.cs b/Core/ContractConnection.QueryResponse.cs new file mode 100644 index 0000000..bfc63fe --- /dev/null +++ b/Core/ContractConnection.QueryResponse.cs @@ -0,0 +1,237 @@ +using MQContract.Attributes; +using MQContract.Interfaces.Service; +using MQContract.Interfaces; +using MQContract.Messages; +using MQContract.Subscriptions; +using System.Reflection; + +namespace MQContract +{ + public partial class ContractConnection + { + private async ValueTask> ProcessPubSubQuery(string? responseChannel, TimeSpan? realTimeout, ServiceMessage serviceMessage, CancellationToken cancellationToken) + where Q : class + where R : class + { + responseChannel ??=typeof(Q).GetCustomAttribute()?.Name; + ArgumentNullException.ThrowIfNullOrWhiteSpace(responseChannel); + var replyChannel = await MapChannel(ChannelMapper.MapTypes.QueryResponse, responseChannel!); + var callID = Guid.NewGuid(); + var (tcs, token) = await QueryResponseHelper.StartResponseListenerAsync( + serviceConnection, + realTimeout??TimeSpan.FromMinutes(1), + indentifier, + callID, + replyChannel, + cancellationToken + ); + var msg = QueryResponseHelper.EncodeMessage( + serviceMessage, + indentifier, + callID, + replyChannel, + null + ); + await serviceConnection.PublishAsync(msg, cancellationToken: cancellationToken); + try + { + await tcs.Task.WaitAsync(cancellationToken); + } + finally + { + if (!token.IsCancellationRequested) + await token.CancelAsync(); + } + return await ProduceResultAsync(tcs.Task.Result); + } + + private async ValueTask ProcessInboxMessage(IInboxQueryableMessageServiceConnection inboxMessageServiceConnection, ServiceMessage serviceMessage, TimeSpan timeout, CancellationToken cancellationToken) + { + var messageID = Guid.NewGuid(); + await inboxSemaphore.WaitAsync(cancellationToken); + inboxSubscription ??= await inboxMessageServiceConnection.EstablishInboxSubscriptionAsync( + async (message) => + { + await inboxSemaphore.WaitAsync(); + if (message.Acknowledge!=null) + await message.Acknowledge(); + if (inboxResponses.TryGetValue(message.CorrelationID,out var taskCompletionSource)) + { + taskCompletionSource.TrySetResult(new( + message.ID, + message.Header, + message.MessageTypeID, + message.Data + )); + } + inboxSemaphore.Release(); + }, + cancellationToken + ); + var tcs = new TaskCompletionSource(); + inboxResponses.Add(messageID, tcs); + inboxSemaphore.Release(); + using var token = new CancellationTokenSource(); + var reg = cancellationToken.Register(() => token.Cancel()); + token.Token.Register(async () => { + await reg.DisposeAsync(); + if (!tcs.Task.IsCompleted) + tcs.TrySetException(new QueryTimeoutException()); + }); + token.CancelAfter(timeout); + var result = await inboxMessageServiceConnection.QueryAsync(serviceMessage, messageID, cancellationToken); + if (result.IsError) + { + await inboxSemaphore.WaitAsync(); + inboxResponses.Remove(messageID); + inboxSemaphore.Release(); + throw new QuerySubmissionFailedException(result.Error!); + } + try + { + await tcs.Task.WaitAsync(cancellationToken); + } + finally + { + if (!token.IsCancellationRequested) + await token.CancelAsync(); + await inboxSemaphore.WaitAsync(); + inboxResponses.Remove(messageID); + inboxSemaphore.Release(); + } + return tcs.Task.Result; + } + + private async ValueTask> ExecuteQueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, string? responseChannel = null, MessageHeader? messageHeader = null, CancellationToken cancellationToken = new CancellationToken()) + where Q : class + where R : class + { + var realTimeout = timeout??typeof(Q).GetCustomAttribute()?.TimeSpanValue; + var serviceMessage = await ProduceServiceMessageAsync(ChannelMapper.MapTypes.Query, GetMessageFactory(), message, false,channel: channel, messageHeader: messageHeader); + if (serviceConnection is IQueryResponseMessageServiceConnection queryableMessageServiceConnection) + return await ProduceResultAsync( + await queryableMessageServiceConnection.QueryAsync( + serviceMessage, + realTimeout??queryableMessageServiceConnection.DefaultTimeout, + cancellationToken + ) + ); + else if (serviceConnection is IInboxQueryableMessageServiceConnection inboxMessageServiceConnection) + return await ProduceResultAsync( + await ProcessInboxMessage(inboxMessageServiceConnection, serviceMessage, realTimeout??inboxMessageServiceConnection.DefaultTimeout,cancellationToken) + ); + return await ProcessPubSubQuery(responseChannel, realTimeout, serviceMessage, cancellationToken); + } + + private async ValueTask> ProduceResultAsync(ServiceQueryResult queryResult) where R : class + { + QueryResult result; + try + { + (var resultMessage, var messageHeader) = await DecodeServiceMessageAsync(ChannelMapper.MapTypes.QueryResponse, GetMessageFactory(true), new(queryResult.ID, queryResult.MessageTypeID, string.Empty, queryResult.Header, queryResult.Data)); + result = new QueryResult( + queryResult.ID, + messageHeader, + Result: resultMessage + ); + } + catch (QueryResponseException qre) + { + return new( + queryResult.ID, + queryResult.Header, + Result: default, + Error: qre.Message + ); + } + catch (Exception ex) + { + return new( + queryResult.ID, + queryResult.Header, + Result: default, + Error: ex.Message + ); + } + return result; + } + private async ValueTask ProduceSubscribeQueryResponseAsync(Func, ValueTask>> messageReceived, Action errorReceived, string? channel, string? group, bool ignoreMessageHeader, bool synchronous, CancellationToken cancellationToken) + where Q : class + where R : class + { + var queryMessageFactory = GetMessageFactory(ignoreMessageHeader); + var responseMessageFactory = GetMessageFactory(); + var subscription = new QueryResponseSubscription( + async (message, replyChannel) => + { + (var taskMessage, var messageHeader) = await DecodeServiceMessageAsync( + ChannelMapper.MapTypes.QuerySubscription, + queryMessageFactory, + message + ); + var result = await messageReceived(new ReceivedMessage(message.ID, taskMessage!, messageHeader, message.ReceivedTimestamp, DateTime.Now)); + return await ProduceServiceMessageAsync( + ChannelMapper.MapTypes.QueryResponse, + responseMessageFactory, + result.Message, + true, + replyChannel, + new(result.Headers) + ); + }, + errorReceived, + (originalChannel) => MapChannel(ChannelMapper.MapTypes.QuerySubscription, originalChannel), + channel: channel, + group: group, + synchronous: synchronous, + logger: logger); + if (await subscription.EstablishSubscriptionAsync(serviceConnection, cancellationToken)) + return subscription; + throw new SubscriptionFailedException(); + } + + async ValueTask> IContractConnection.QueryAsync(Q message, TimeSpan? timeout, string? channel, string? responseChannel, MessageHeader? messageHeader, CancellationToken cancellationToken) + => await ExecuteQueryAsync(message, timeout: timeout, channel: channel, responseChannel: responseChannel, messageHeader: messageHeader, cancellationToken: cancellationToken); + + async ValueTask> IContractConnection.QueryAsync(Q message, TimeSpan? timeout, string? channel, string? responseChannel, MessageHeader? messageHeader, + CancellationToken cancellationToken) + { +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + var responseType = (typeof(Q).GetCustomAttribute(false)?.ResponseType)??throw new UnknownResponseTypeException("ResponseType", typeof(Q)); +#pragma warning restore CA2208 // Instantiate argument exceptions correctly +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + var methodInfo = typeof(ContractConnection).GetMethod(nameof(ContractConnection.ExecuteQueryAsync), BindingFlags.NonPublic | BindingFlags.Instance)!.MakeGenericMethod(typeof(Q), responseType!); +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + dynamic? queryResult; + try + { + queryResult = (dynamic?)await Utility.InvokeMethodAsync( + methodInfo, + this, [ + message, + timeout, + channel, + responseChannel, + messageHeader, + cancellationToken + ] + ); + } + catch (TimeoutException) + { + throw new QueryTimeoutException(); + } + return new QueryResult(queryResult?.ID??string.Empty, queryResult?.Header??new MessageHeader([]), queryResult?.Result, queryResult?.Error); + } + + ValueTask IContractConnection.SubscribeQueryAsyncResponseAsync(Func, ValueTask>> messageReceived, Action errorReceived, string? channel, string? group, bool ignoreMessageHeader, CancellationToken cancellationToken) + => ProduceSubscribeQueryResponseAsync(messageReceived, errorReceived, channel, group, ignoreMessageHeader, false, cancellationToken); + + ValueTask IContractConnection.SubscribeQueryResponseAsync(Func, QueryResponseMessage> messageReceived, Action errorReceived, string? channel, string? group, bool ignoreMessageHeader, CancellationToken cancellationToken) + => ProduceSubscribeQueryResponseAsync((msg) => + { + var result = messageReceived(msg); + return ValueTask.FromResult(result); + }, errorReceived, channel, group, ignoreMessageHeader, true, cancellationToken); + } +} diff --git a/Core/ContractConnection.cs b/Core/ContractConnection.cs index 325874d..594890f 100644 --- a/Core/ContractConnection.cs +++ b/Core/ContractConnection.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using MQContract.Attributes; using MQContract.Factories; using MQContract.Interfaces; using MQContract.Interfaces.Encoding; @@ -7,32 +6,66 @@ using MQContract.Interfaces.Factories; using MQContract.Interfaces.Service; using MQContract.Messages; -using MQContract.Subscriptions; -using System.Reflection; +using MQContract.Middleware; namespace MQContract { /// - /// This is the primary class for this library and is used to create a Contract style connection between systems using the underlying service connection layer. + /// The primary ContractConnection item which implements IContractConnection /// - /// The service connection implementation to use for the underlying message requests. - /// A default message encoder implementation if desired. If there is no specific encoder for a given type, this encoder would be called. The built in default being used dotnet Json serializer. - /// A default message encryptor implementation if desired. If there is no specific encryptor - /// A service prodivder instance supplied in the case that dependency injection might be necessary - /// An instance of a logger if logging is desired - /// An instance of a ChannelMapper used to translate channels from one instance to another based on class channel attributes or supplied channels if necessary. - /// For example, it might be necessary for a Nats.IO instance when you are trying to read from a stored message stream that is comprised of another channel or set of channels - /// - public class ContractConnection(IMessageServiceConnection serviceConnection, + public sealed partial class ContractConnection + : IContractConnection + { + private readonly Guid indentifier = Guid.NewGuid(); + private readonly SemaphoreSlim dataLock = new(1, 1); + private readonly IMessageServiceConnection serviceConnection; + private readonly IMessageEncoder? defaultMessageEncoder; + private readonly IMessageEncryptor? defaultMessageEncryptor; + private readonly IServiceProvider? serviceProvider; + private readonly ILogger? logger; + private readonly ChannelMapper? channelMapper; + private readonly List middleware; + private readonly SemaphoreSlim inboxSemaphore = new(1, 1); + private readonly Dictionary> inboxResponses = []; + private IServiceSubscription? inboxSubscription; + private IEnumerable typeFactories = []; + private bool disposedValue; + + private ContractConnection(IMessageServiceConnection serviceConnection, IMessageEncoder? defaultMessageEncoder = null, IMessageEncryptor? defaultMessageEncryptor = null, IServiceProvider? serviceProvider = null, ILogger? logger = null, ChannelMapper? channelMapper = null) - : IContractConnection - { - private readonly SemaphoreSlim dataLock = new(1, 1); - private IEnumerable typeFactories = []; + { + this.serviceConnection = serviceConnection; + this.defaultMessageEncoder = defaultMessageEncoder; + this.defaultMessageEncryptor= defaultMessageEncryptor; + this.serviceProvider = serviceProvider; + this.logger=logger; + this.channelMapper=channelMapper; + this.middleware= [new ChannelMappingMiddleware(this.channelMapper)]; + } + + /// + /// This is the call used to create an instance of a Contract Connection which will return the Interface + /// + /// The service connection implementation to use for the underlying message requests. + /// A default message encoder implementation if desired. If there is no specific encoder for a given type, this encoder would be called. The built in default being used dotnet Json serializer. + /// A default message encryptor implementation if desired. If there is no specific encryptor + /// A service prodivder instance supplied in the case that dependency injection might be necessary + /// An instance of a logger if logging is desired + /// An instance of a ChannelMapper used to translate channels from one instance to another based on class channel attributes or supplied channels if necessary. + /// For example, it might be necessary for a Nats.IO instance when you are trying to read from a stored message stream that is comprised of another channel or set of channels + /// + /// An instance of IContractConnection + public static IContractConnection Instance(IMessageServiceConnection serviceConnection, + IMessageEncoder? defaultMessageEncoder = null, + IMessageEncryptor? defaultMessageEncryptor = null, + IServiceProvider? serviceProvider = null, + ILogger? logger = null, + ChannelMapper? channelMapper = null) + => new ContractConnection(serviceConnection,defaultMessageEncoder,defaultMessageEncryptor,serviceProvider,logger, channelMapper); private IMessageFactory GetMessageFactory(bool ignoreMessageHeader = false) where T : class { @@ -50,188 +83,59 @@ private IMessageFactory GetMessageFactory(bool ignoreMessageHeader = false return result; } - private Task MapChannel(ChannelMapper.MapTypes mapType, string originalChannel) - => channelMapper?.MapChannel(mapType, originalChannel)??Task.FromResult(originalChannel); + private ValueTask MapChannel(ChannelMapper.MapTypes mapType, string originalChannel) + => channelMapper?.MapChannel(mapType, originalChannel)??ValueTask.FromResult(originalChannel); - /// - /// Called to execute a ping against the service layer - /// - /// The ping result from the service layer, if supported - public Task PingAsync() - => serviceConnection.PingAsync(); + ValueTask IContractConnection.PingAsync() + => (serviceConnection is IPingableMessageServiceConnection pingableService ? pingableService.PingAsync() : throw new NotSupportedException("The underlying service does not support Ping")); - /// - /// Called to publish a message out into the service layer in the Pub/Sub style - /// - /// The type of message to publish - /// The instance of the message to publish - /// Used to override the MessageChannelAttribute from the class or to specify a channel to transmit the message on - /// A message header to be sent across with the message - /// An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary - /// A cancellation token - /// An instance of the TransmissionResult record to indicate success or failure and an ID - public async Task PublishAsync(T message, string? channel = null, MessageHeader? messageHeader = null, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) - where T : class - => await serviceConnection.PublishAsync( - await ProduceServiceMessage(ChannelMapper.MapTypes.Publish,message, channel: channel, messageHeader: messageHeader), - options, - cancellationToken - ); - - private async Task ProduceServiceMessage(ChannelMapper.MapTypes mapType,T message, string? channel = null, MessageHeader? messageHeader = null) where T : class - => await GetMessageFactory().ConvertMessageAsync(message, channel, messageHeader,(originalChannel)=>MapChannel(mapType,originalChannel)); - - /// - /// Called to establish a Subscription in the sevice layer for the Pub/Sub style messaging - /// - /// The type of message to listen for - /// The callback to be executed when a message is recieved - /// The callback to be executed when an error occurs - /// Used to override the MessageChannelAttribute from the class or to specify a channel to listen for messages on - /// Used to specify a group to associate to at the service layer (refer to groups in KubeMQ, Nats.IO, etc) - /// If set to true this will cause the subscription to ignore the message type specified and assume that the type of message is of type T - /// Set true if the desire the messageRecieved callback to be called such that it waits for the call to complete prior to calling for the next message - /// An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary - /// A cancellation token - /// An instance of the Subscription that can be held or called to end - /// An exception thrown when the subscription has failed to establish - public async Task SubscribeAsync(Func,Task> messageRecieved, Action errorRecieved, string? channel = null, string? group = null, bool ignoreMessageHeader = false, bool synchronous = false, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) where T : class + async ValueTask IContractConnection.CloseAsync() { - var subscription = new PubSubSubscription(GetMessageFactory(ignoreMessageHeader), - messageRecieved, - errorRecieved, - (originalChannel)=>MapChannel(ChannelMapper.MapTypes.PublishSubscription,originalChannel), - channel:channel, - group:group, - synchronous:synchronous, - options:options, - logger:logger); - if (await subscription.EstablishSubscriptionAsync(serviceConnection,cancellationToken)) - return subscription; - throw new SubscriptionFailedException(); + await (inboxSubscription?.EndAsync()??ValueTask.CompletedTask); + await (serviceConnection?.CloseAsync()??ValueTask.CompletedTask); } - private async Task> ExecuteQueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, MessageHeader? messageHeader = null, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) - where Q : class - where R : class - => ProduceResultAsync(await serviceConnection.QueryAsync( - await ProduceServiceMessage(ChannelMapper.MapTypes.Query,message, channel: channel, messageHeader: messageHeader), - timeout??TimeSpan.FromMilliseconds(typeof(Q).GetCustomAttribute()?.Value??serviceConnection.DefaultTimout.TotalMilliseconds), - options, - cancellationToken - )); - - /// - /// Called to publish a message in the Query/Response style - /// - /// The type of message to transmit for the Query - /// The type of message expected as a response - /// The message to transmit for the query - /// The timeout to allow for waiting for a response - /// Used to override the MessageChannelAttribute from the class or to specify a channel to transmit the message on - /// A message header to be sent across with the message - /// An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary - /// A cancellation token - /// A QueryResult that will contain the response message and or an error - public async Task> QueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, MessageHeader? messageHeader = null, IServiceChannelOptions? options = null, CancellationToken cancellationToken = new CancellationToken()) - where Q : class - where R : class - => await ExecuteQueryAsync(message, timeout: timeout, channel: channel, messageHeader: messageHeader, options: options, cancellationToken: cancellationToken); - - /// - /// Called to publish a message in the Query/Response style except the response Type is gathered from the QueryResponseTypeAttribute - /// - /// The type of message to transmit for the Query - /// The message to transmit for the query - /// The timeout to allow for waiting for a response - /// Used to override the MessageChannelAttribute from the class or to specify a channel to transmit the message on - /// A message header to be sent across with the message - /// An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary - /// A cancellation token - /// A QueryResult that will contain the response message and or an error - /// Thrown when the supplied Query type does not have a QueryResponseTypeAttribute and therefore a response type cannot be determined - public async Task> QueryAsync(Q message, TimeSpan? timeout = null, string? channel = null, MessageHeader? messageHeader = null, - IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) where Q : class + private void Dispose(bool disposing) { - var responseType = (typeof(Q).GetCustomAttribute(false)?.ResponseType)??throw new UnknownResponseTypeException("ResponseType", typeof(Q)); -#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields - var methodInfo = typeof(ContractConnection).GetMethod(nameof(ContractConnection.ExecuteQueryAsync), BindingFlags.NonPublic | BindingFlags.Instance)!.MakeGenericMethod(typeof(Q), responseType!); -#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields - var task = (Task)methodInfo.Invoke(this, [ - message, - timeout, - channel, - messageHeader, - options, - cancellationToken - ])!; - await task.ConfigureAwait(false); - var queryResult = ((dynamic)task).Result; - return new QueryResult(queryResult.ID, queryResult.Header, queryResult.Result, queryResult.Error); + if (!disposedValue) + { + if (disposing) + { + if (inboxSubscription is IDisposable subDisposable) + subDisposable.Dispose(); + else if (inboxSubscription is IAsyncDisposable asyncSubDisposable) + asyncSubDisposable.DisposeAsync().AsTask().Wait(); + if (serviceConnection is IDisposable disposable) + disposable.Dispose(); + else if (serviceConnection is IAsyncDisposable asyncDisposable) + asyncDisposable.DisposeAsync().AsTask().Wait(); + } + dataLock.Dispose(); + inboxSemaphore.Dispose(); + disposedValue=true; + } } - private QueryResult ProduceResultAsync(ServiceQueryResult queryResult) where R : class + void IDisposable.Dispose() { - try - { - return new( - queryResult.ID, - queryResult.Header, - Result: GetMessageFactory().ConvertMessage(logger, queryResult) - ); - }catch(QueryResponseException qre) - { - return new( - queryResult.ID, - queryResult.Header, - Result: default, - Error: qre.Message - ); - } - catch(Exception ex) - { - return new( - queryResult.ID, - queryResult.Header, - Result: default, - Error: ex.Message - ); - } + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } - /// - /// Creates a subscription with the underlying service layer for the Query/Response style - /// - /// The expected message type for the Query - /// The expected message type for the Response - /// The callback to be executed when a message is recieved and expects a returned response - /// The callback to be executed when an error occurs - /// Used to override the MessageChannelAttribute from the class or to specify a channel to listen for messages on - /// Used to specify a group to associate to at the service layer (refer to groups in KubeMQ, Nats.IO, etc) - /// If set to true this will cause the subscription to ignore the message type specified and assume that the type of message is of type T - /// Set true if the desire the messageRecieved callback to be called such that it waits for the call to complete prior to calling for the next message - /// An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary - /// A cancellation token - /// An instance of the Subscription that can be held or called to end - /// An exception thrown when the subscription has failed to establish - public async Task SubscribeQueryResponseAsync(Func, Task>> messageRecieved, Action errorRecieved, string? channel = null, string? group = null, bool ignoreMessageHeader = false, bool synchronous = false, IServiceChannelOptions? options = null, CancellationToken cancellationToken = default) - where Q : class - where R : class + async ValueTask IAsyncDisposable.DisposeAsync() { - var subscription = new QueryResponseSubscription( - GetMessageFactory(ignoreMessageHeader), - GetMessageFactory(), - messageRecieved, - errorRecieved, - (originalChannel) => MapChannel(ChannelMapper.MapTypes.QuerySubscription, originalChannel), - channel: channel, - group: group, - synchronous: synchronous, - options: options, - logger: logger); - if (await subscription.EstablishSubscriptionAsync(serviceConnection, cancellationToken)) - return subscription; - throw new SubscriptionFailedException(); + if (inboxSubscription is IAsyncDisposable asyncSubDisposable) + asyncSubDisposable.DisposeAsync().AsTask().Wait(); + else if (inboxSubscription is IDisposable subDisposable) + subDisposable.Dispose(); + if (serviceConnection is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(true); + else if (serviceConnection is IDisposable disposable) + disposable.Dispose(); + + Dispose(false); + GC.SuppressFinalize(this); } } } diff --git a/Core/Core.csproj b/Core/Core.csproj index 68d6ef1..d88ece6 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -1,26 +1,15 @@  + + net8.0 enable enable MQContract MQContract - true - $(MSBuildProjectDirectory)\Readme.md - True - roger-castaldo Core for MQContract $(AssemblyName) - https://github.com/roger-castaldo/MQContract - Readme.md - https://github.com/roger-castaldo/MQContract - Message Queue MQ Contract - MIT - True - True - True - snupkg diff --git a/Core/Defaults/JsonEncoder.cs b/Core/Defaults/JsonEncoder.cs index 60f73db..5516b90 100644 --- a/Core/Defaults/JsonEncoder.cs +++ b/Core/Defaults/JsonEncoder.cs @@ -14,10 +14,14 @@ internal class JsonEncoder : IMessageTypeEncoder ReadCommentHandling=JsonCommentHandling.Skip }; - public T? Decode(Stream stream) - => JsonSerializer.Deserialize(stream, options: JsonOptions); + public async ValueTask DecodeAsync(Stream stream) + => await JsonSerializer.DeserializeAsync(stream, options: JsonOptions); - public byte[] Encode(T message) - => System.Text.UTF8Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message, options: JsonOptions)); + public async ValueTask EncodeAsync(T message) + { + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, message, options: JsonOptions); + return ms.ToArray(); + } } } diff --git a/Core/Defaults/NonEncryptor.cs b/Core/Defaults/NonEncryptor.cs index d91b95b..a14c1ce 100644 --- a/Core/Defaults/NonEncryptor.cs +++ b/Core/Defaults/NonEncryptor.cs @@ -5,12 +5,13 @@ namespace MQContract.Defaults { internal class NonEncryptor : IMessageTypeEncryptor { - public Stream Decrypt(Stream stream, MessageHeader headers) => stream; + public ValueTask DecryptAsync(Stream stream, MessageHeader headers) + => ValueTask.FromResult(stream); - public byte[] Encrypt(byte[] data, out Dictionary headers) + public ValueTask EncryptAsync(byte[] data, out Dictionary headers) { headers = []; - return data; + return ValueTask.FromResult(data); } } } diff --git a/Core/Exceptions.cs b/Core/Exceptions.cs index 72fb207..7e68ce6 100644 --- a/Core/Exceptions.cs +++ b/Core/Exceptions.cs @@ -45,4 +45,40 @@ public class QueryResponseException : Exception internal QueryResponseException(string message) : base(message) { } } + + /// + /// Thrown when a query call is being made to a service that does not support query response and the listener cannot be created + /// + public class QueryExecutionFailedException : Exception + { + internal QueryExecutionFailedException() + : base("Failed to execute query") { } + } + + /// + /// Thrown when a query call is being made to an inbox style service and the message fails to transmit + /// + public class QuerySubmissionFailedException : Exception + { + internal QuerySubmissionFailedException(string message) + : base(message) { } + } + + /// + /// Thrown when a query call times out waiting for the response + /// + public class QueryTimeoutException : Exception + { + internal QueryTimeoutException() + : base("Query Response request timed out") { } + } + + /// + /// Thrown when a query call message is received without proper data + /// + public class InvalidQueryResponseMessageReceived : Exception + { + internal InvalidQueryResponseMessageReceived() + : base("A service message was received on a query response channel without the proper data") { } + } } diff --git a/Core/Factories/ConversionPath.cs b/Core/Factories/ConversionPath.cs index c53744c..e487e82 100644 --- a/Core/Factories/ConversionPath.cs +++ b/Core/Factories/ConversionPath.cs @@ -31,27 +31,30 @@ public ConversionPath(IEnumerable path, IEnumerable types, IMessag messageEncryptor = (IMessageTypeEncryptor)(serviceProvider!=null ? ActivatorUtilities.CreateInstance(serviceProvider, encryptorType) : Activator.CreateInstance(encryptorType)!); } - public V? ConvertMessage(ILogger? logger, IEncodedMessage message, Stream? dataStream = null) + public async ValueTask ConvertMessageAsync(ILogger? logger, IEncodedMessage message, Stream? dataStream = null) { - dataStream = (globalMessageEncryptor!=null && messageEncryptor is NonEncryptor ? globalMessageEncryptor : messageEncryptor).Decrypt(dataStream??new MemoryStream(message.Data.ToArray()), message.Header); - object? result = (globalMessageEncoder!=null && messageEncoder is JsonEncoder ? globalMessageEncoder.Decode(dataStream) : messageEncoder.Decode(dataStream)); + dataStream = await (globalMessageEncryptor!=null && messageEncryptor is NonEncryptor ? globalMessageEncryptor : messageEncryptor).DecryptAsync(dataStream??new MemoryStream(message.Data.ToArray()), message.Header); + object? result = await (globalMessageEncoder!=null && messageEncoder is JsonEncoder ? globalMessageEncoder.DecodeAsync(dataStream) : messageEncoder.DecodeAsync(dataStream)); foreach (var converter in path) { logger?.LogTrace("Attempting to convert {SourceType} to {DestiniationType} through converters for {IntermediateType}", Utility.TypeName(), Utility.TypeName(), Utility.TypeName(ExtractGenericArguements(converter.GetType())[0])); - result = ExecuteConverter(converter, result, ExtractGenericArguements(converter.GetType())[1]); + result = await ExecuteConverter(converter, result, ExtractGenericArguements(converter.GetType())[1]); } return (V?)result; } private static Type[] ExtractGenericArguements(Type t) => t.GetInterfaces().First(iface => iface.IsGenericType && iface.GetGenericTypeDefinition()==typeof(IMessageConverter<,>)).GetGenericArguments(); - private static object? ExecuteConverter(object converter, object? source, Type destination) + private static async ValueTask ExecuteConverter(object converter, object? source, Type destination) { if (source==null) return null; - return typeof(IMessageConverter<,>).MakeGenericType(source.GetType(), destination) - .GetMethod("Convert")! - .Invoke(converter, [source]); + return await Utility.InvokeMethodAsync( + typeof(IMessageConverter<,>).MakeGenericType(source.GetType(), destination) + .GetMethod("ConvertAsync")!, + converter, + [source] + ); } } } diff --git a/Core/Factories/MessageTypeFactory.cs b/Core/Factories/MessageTypeFactory.cs index 413d4b5..6036951 100644 --- a/Core/Factories/MessageTypeFactory.cs +++ b/Core/Factories/MessageTypeFactory.cs @@ -25,14 +25,14 @@ internal class MessageTypeFactory private readonly IMessageTypeEncoder? messageEncoder; private readonly IMessageTypeEncryptor? messageEncryptor; private readonly IEnumerable> converters; - private readonly int maxMessageSize; + private readonly uint maxMessageSize; public bool IgnoreMessageHeader { get; private init; } - private readonly string messageName = typeof(T).GetCustomAttributes().Select(mn => mn.Value).FirstOrDefault(Utility.TypeName()); - private readonly string messageVersion = typeof(T).GetCustomAttributes().Select(mc => mc.Version.ToString()).FirstOrDefault("0.0.0.0"); - private readonly string messageChannel = typeof(T).GetCustomAttributes().Select(mc => mc.Name).FirstOrDefault(string.Empty); + private readonly string messageName = Utility.MessageTypeName(); + private readonly string messageVersion = Utility.MessageVersionString(); + public string? MessageChannel => typeof(T).GetCustomAttributes().Select(mc => mc.Name).FirstOrDefault(); - public MessageTypeFactory(IMessageEncoder? globalMessageEncoder, IMessageEncryptor? globalMessageEncryptor, IServiceProvider? serviceProvider, bool ignoreMessageHeader, int? maxMessageSize) + public MessageTypeFactory(IMessageEncoder? globalMessageEncoder, IMessageEncryptor? globalMessageEncryptor, IServiceProvider? serviceProvider, bool ignoreMessageHeader, uint? maxMessageSize) { this.maxMessageSize = maxMessageSize??int.MaxValue; this.globalMessageEncryptor = globalMessageEncryptor; @@ -146,21 +146,20 @@ private static bool IsMessageTypeMatch(string metaData, Type t, out bool isCompr return false; } - public async Task ConvertMessageAsync(T message, string? channel, MessageHeader? messageHeader, Func>? mapChannel=null) + public async ValueTask ConvertMessageAsync(T message,bool ignoreChannel, string? channel, MessageHeader messageHeader) { - channel ??= messageChannel; - if (string.IsNullOrWhiteSpace(channel)) + if (string.IsNullOrWhiteSpace(channel)&&!ignoreChannel) throw new MessageChannelNullException(); - var encodedData = messageEncoder?.Encode(message)??globalMessageEncoder!.Encode(message); - var body = messageEncryptor?.Encrypt(encodedData, out var messageHeaders)??globalMessageEncryptor!.Encrypt(encodedData, out messageHeaders); + var encodedData = await (messageEncoder?.EncodeAsync(message)??globalMessageEncoder!.EncodeAsync(message)); + var body = await (messageEncryptor?.EncryptAsync(encodedData, out var messageHeaders)??globalMessageEncryptor!.EncryptAsync(encodedData, out messageHeaders)); var metaData = string.Empty; if (body.Length>maxMessageSize) { using var ms = new MemoryStream(); var zip = new GZipStream(ms, System.IO.Compression.CompressionLevel.SmallestSize, false); - await zip.WriteAsync(body, 0, body.Length); + await zip.WriteAsync(body); await zip.FlushAsync(); body = ms.ToArray(); metaData = "C"; @@ -171,13 +170,11 @@ public async Task ConvertMessageAsync(T message, string? channel else metaData="U"; metaData+=$"-{messageName}-{messageVersion}"; - if (mapChannel!=null) - channel=await mapChannel(channel); - return new ServiceMessage(Guid.NewGuid().ToString(), metaData, channel, new MessageHeader(messageHeader, messageHeaders), body); + return new ServiceMessage(Guid.NewGuid().ToString(), metaData, channel??string.Empty, new MessageHeader(messageHeader, messageHeaders), body); } - T? IConversionPath.ConvertMessage(ILogger? logger, IEncodedMessage message, Stream? dataStream) + async ValueTask IConversionPath.ConvertMessageAsync(ILogger? logger, IEncodedMessage message, Stream? dataStream) { if (!IgnoreMessageHeader) #pragma warning disable S3236 // Caller information arguments should not be provided explicitly @@ -191,11 +188,11 @@ public async Task ConvertMessageAsync(T message, string? channel if (IgnoreMessageHeader || IsMessageTypeMatch(message.MessageTypeID, typeof(T), out compressed)) { dataStream = (compressed ? new GZipStream(new MemoryStream(message.Data.ToArray()), System.IO.Compression.CompressionMode.Decompress) : new MemoryStream(message.Data.ToArray())); - dataStream = messageEncryptor?.Decrypt(dataStream, message.Header)??globalMessageEncryptor!.Decrypt(dataStream, message.Header); + dataStream = await (messageEncryptor?.DecryptAsync(dataStream, message.Header)??globalMessageEncryptor!.DecryptAsync(dataStream, message.Header)); if (messageEncoder!=null) - result = messageEncoder.Decode(dataStream); + result = await messageEncoder.DecodeAsync(dataStream); else - result = globalMessageEncoder!.Decode(dataStream); + result = await globalMessageEncoder!.DecodeAsync(dataStream); } else { @@ -210,7 +207,7 @@ public async Task ConvertMessageAsync(T message, string? channel if (converter==null) throw new InvalidCastException(); dataStream = (compressed ? new GZipStream(new MemoryStream(message.Data.ToArray()), System.IO.Compression.CompressionMode.Decompress) : new MemoryStream(message.Data.ToArray())); - result = converter.ConvertMessage(logger, message, dataStream: dataStream); + result = await converter.ConvertMessageAsync(logger, message, dataStream: dataStream); } if (Equals(result, default(T?))) throw new MessageConversionException(typeof(T), converter?.GetType()??GetType()); diff --git a/Core/Interfaces/Conversion/IConversionPath.cs b/Core/Interfaces/Conversion/IConversionPath.cs index 63b9a2a..ead9f2e 100644 --- a/Core/Interfaces/Conversion/IConversionPath.cs +++ b/Core/Interfaces/Conversion/IConversionPath.cs @@ -3,9 +3,9 @@ namespace MQContract.Interfaces.Conversion { - internal interface IConversionPath + internal interface IConversionPath where T : class { - T? ConvertMessage(ILogger? logger, IEncodedMessage message, Stream? dataStream = null); + ValueTask ConvertMessageAsync(ILogger? logger, IEncodedMessage message, Stream? dataStream = null); } } diff --git a/Core/Interfaces/Factories/IMessageFactory.cs b/Core/Interfaces/Factories/IMessageFactory.cs index 2d0efe1..b3cc59b 100644 --- a/Core/Interfaces/Factories/IMessageFactory.cs +++ b/Core/Interfaces/Factories/IMessageFactory.cs @@ -5,6 +5,7 @@ namespace MQContract.Interfaces.Factories { internal interface IMessageFactory : IMessageTypeFactory, IConversionPath where T : class { - Task ConvertMessageAsync(T message, string? channel, MessageHeader? messageHeader,Func>? mapChannel=null); + string? MessageChannel { get; } + ValueTask ConvertMessageAsync(T message,bool ignoreChannel, string? channel, MessageHeader messageHeader); } } diff --git a/Core/Messages/RecievedMessage.cs b/Core/Messages/RecievedMessage.cs index 3ecb28f..6c779bf 100644 --- a/Core/Messages/RecievedMessage.cs +++ b/Core/Messages/RecievedMessage.cs @@ -2,8 +2,8 @@ namespace MQContract.Messages { - internal record RecievedMessage(string ID,T Message,MessageHeader Headers,DateTime RecievedTimestamp,DateTime ProcessedTimestamp) - : IRecievedMessage + internal record ReceivedMessage(string ID,T Message,MessageHeader Headers,DateTime ReceivedTimestamp,DateTime ProcessedTimestamp) + : IReceivedMessage where T : class {} } diff --git a/Core/Middleware/ChannelMappingMiddleware.cs b/Core/Middleware/ChannelMappingMiddleware.cs new file mode 100644 index 0000000..1dd6e50 --- /dev/null +++ b/Core/Middleware/ChannelMappingMiddleware.cs @@ -0,0 +1,19 @@ +using MQContract.Interfaces.Middleware; +using MQContract.Messages; + +namespace MQContract.Middleware +{ + internal class ChannelMappingMiddleware(ChannelMapper? channelMapper) + : IBeforeEncodeMiddleware + { + private async ValueTask MapChannel(Context context,string? channel) + { + if (channelMapper==null || channel==null) + return channel; + return await channelMapper.MapChannel(context.MapDirection, channel); + } + + public async ValueTask<(T message, string? channel, MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context, T message, string? channel, MessageHeader messageHeader) + => (message, await MapChannel((Context)context,channel), messageHeader); + } +} diff --git a/Core/Middleware/Context.cs b/Core/Middleware/Context.cs new file mode 100644 index 0000000..46464d9 --- /dev/null +++ b/Core/Middleware/Context.cs @@ -0,0 +1,29 @@ +using MQContract.Interfaces.Middleware; + +namespace MQContract.Middleware +{ + internal class Context : IContext + { + private const string MapTypeKey = "_MapType"; + private readonly Dictionary values = []; + + public Context(ChannelMapper.MapTypes mapDirection) + { + this[MapTypeKey] = mapDirection; + } + + public object? this[string key] { + get => values.TryGetValue(key, out var value) ? value : null; + set + { + if (value==null) + values.Remove(key); + else + values.TryAdd(key, value); + } + } + + public ChannelMapper.MapTypes MapDirection + => (ChannelMapper.MapTypes)this[MapTypeKey]!; + } +} diff --git a/Core/Middleware/Metrics/ContractMetric.cs b/Core/Middleware/Metrics/ContractMetric.cs new file mode 100644 index 0000000..2c5bec1 --- /dev/null +++ b/Core/Middleware/Metrics/ContractMetric.cs @@ -0,0 +1,36 @@ +using MQContract.Interfaces; + +namespace MQContract.Middleware.Metrics +{ + internal record ContractMetric : IContractMetric + { + public ulong Messages { get; private set; } = 0; + + public ulong MessageBytes { get; private set; } = 0; + + public ulong MessageBytesAverage => (Messages==0 ? 0 : MessageBytes / Messages); + + public ulong MessageBytesMin { get; private set; } = ulong.MaxValue; + + public ulong MessageBytesMax { get; private set; } = ulong.MinValue; + + public TimeSpan MessageConversionDuration { get; private set; } = TimeSpan.Zero; + + public TimeSpan MessageConversionAverage => (Messages==0 ? TimeSpan.Zero : MessageConversionDuration / Messages); + + public TimeSpan MessageConversionMin { get; private set; } = TimeSpan.MaxValue; + + public TimeSpan MessageConversionMax { get; private set; } = TimeSpan.MinValue; + + public void AddMessageRecord(int messageSize, TimeSpan encodingDuration) + { + Messages++; + MessageBytes += (ulong)messageSize; + MessageBytesMin = Math.Min(MessageBytesMin, (ulong)messageSize); + MessageBytesMax = Math.Max(MessageBytesMax, (ulong)messageSize); + MessageConversionDuration += encodingDuration; + MessageConversionMin = TimeSpan.FromTicks(Math.Min(MessageConversionMin.Ticks, encodingDuration.Ticks)); + MessageConversionMax = TimeSpan.FromTicks(Math.Max(MessageConversionMin.Ticks, encodingDuration.Ticks)); + } + } +} diff --git a/Core/Middleware/Metrics/InternalMetricTracker.cs b/Core/Middleware/Metrics/InternalMetricTracker.cs new file mode 100644 index 0000000..dcaf3a7 --- /dev/null +++ b/Core/Middleware/Metrics/InternalMetricTracker.cs @@ -0,0 +1,82 @@ +namespace MQContract.Middleware.Metrics +{ + internal class InternalMetricTracker + { + private readonly SemaphoreSlim semDataLock = new(1, 1); + private readonly ContractMetric sentGlobalMetric = new(); + private readonly ContractMetric receivedGlobalMetric = new(); + private readonly Dictionary sentTypeMetrics = []; + private readonly Dictionary receivedTypeMetrics = []; + private readonly Dictionary sentChannelMetrics = []; + private readonly Dictionary receivedChannelMetrics = []; + + public void AppendEntry(MetricEntryValue entry) { + semDataLock.Wait(); + ContractMetric? channelMetric = null; + ContractMetric? typeMetric = null; + if (entry.Sent) + { + sentGlobalMetric.AddMessageRecord(entry.MessageSize, entry.Duration); + if (!sentTypeMetrics.TryGetValue(entry.Type, out typeMetric)) + { + typeMetric = new(); + sentTypeMetrics.Add(entry.Type, typeMetric); + } + if (!string.IsNullOrWhiteSpace(entry.Channel) && !sentChannelMetrics.TryGetValue(entry.Channel, out channelMetric)) + { + channelMetric = new(); + sentChannelMetrics.Add(entry.Channel, channelMetric); + } + } + else + { + receivedGlobalMetric.AddMessageRecord(entry.MessageSize, entry.Duration); + if (!receivedTypeMetrics.TryGetValue(entry.Type, out typeMetric)) + { + typeMetric = new(); + receivedTypeMetrics.Add(entry.Type, typeMetric); + } + if (!string.IsNullOrWhiteSpace(entry.Channel) && !receivedChannelMetrics.TryGetValue(entry.Channel, out channelMetric)) + { + channelMetric = new(); + receivedChannelMetrics.Add(entry.Channel, channelMetric); + } + } + typeMetric?.AddMessageRecord(entry.MessageSize,entry.Duration); + channelMetric?.AddMessageRecord(entry.MessageSize, entry.Duration); + semDataLock.Release(); + } + + public ReadonlyContractMetric GetSnapshot(bool sent) + { + semDataLock.Wait(); + var result = new ReadonlyContractMetric((sent ? sentGlobalMetric : receivedGlobalMetric)); + semDataLock.Release(); + return result; + } + + public ReadonlyContractMetric? GetSnapshot(Type messageType,bool sent) + { + ReadonlyContractMetric? result = null; + semDataLock.Wait(); + if (sent && sentTypeMetrics.TryGetValue(messageType, out var sentValue)) + result = new ReadonlyContractMetric(sentValue!); + else if (!sent && receivedTypeMetrics.TryGetValue(messageType, out var receivedValue)) + result = new ReadonlyContractMetric(receivedValue!); + semDataLock.Release(); + return result; + } + + public ReadonlyContractMetric? GetSnapshot(string channel, bool sent) + { + ReadonlyContractMetric? result = null; + semDataLock.Wait(); + if (sent && sentChannelMetrics.TryGetValue(channel, out var sentValue)) + result = new ReadonlyContractMetric(sentValue!); + else if (!sent && receivedChannelMetrics.TryGetValue(channel, out var receivedValue)) + result = new ReadonlyContractMetric(receivedValue!); + semDataLock.Release(); + return result; + } + } +} diff --git a/Core/Middleware/Metrics/MessageMetric.cs b/Core/Middleware/Metrics/MessageMetric.cs new file mode 100644 index 0000000..89f9603 --- /dev/null +++ b/Core/Middleware/Metrics/MessageMetric.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.Metrics; + +namespace MQContract.Middleware.Metrics +{ + internal record MessageMetric(UpDownCounter Sent, UpDownCounter SentBytes, UpDownCounter Received, UpDownCounter ReceivedBytes, + Histogram EncodingDuration, Histogram DecodingDuration) + { + public void AddEntry(MetricEntryValue entry) + { + if (entry.Sent) + { + Sent.Add(1); + SentBytes.Add(entry.MessageSize); + EncodingDuration.Record(entry.Duration.TotalMilliseconds); + } + else + { + Received.Add(1); + ReceivedBytes.Add(entry.MessageSize); + DecodingDuration.Record(entry.Duration.TotalMilliseconds); + } + } + } +} diff --git a/Core/Middleware/Metrics/MetricEntryValue.cs b/Core/Middleware/Metrics/MetricEntryValue.cs new file mode 100644 index 0000000..0f1eee0 --- /dev/null +++ b/Core/Middleware/Metrics/MetricEntryValue.cs @@ -0,0 +1,5 @@ +namespace MQContract.Middleware.Metrics +{ + internal record MetricEntryValue(Type Type, string? Channel,bool Sent, int MessageSize, TimeSpan Duration) + { } +} diff --git a/Core/Middleware/Metrics/ReadonlyContractMetric.cs b/Core/Middleware/Metrics/ReadonlyContractMetric.cs new file mode 100644 index 0000000..f6ee94c --- /dev/null +++ b/Core/Middleware/Metrics/ReadonlyContractMetric.cs @@ -0,0 +1,15 @@ +using MQContract.Interfaces; + +namespace MQContract.Middleware.Metrics +{ + internal record ReadonlyContractMetric(ulong Messages,ulong MessageBytes,ulong MessageBytesAverage, ulong MessageBytesMin, ulong MessageBytesMax, + TimeSpan MessageConversionDuration,TimeSpan MessageConversionAverage,TimeSpan MessageConversionMin, TimeSpan MessageConversionMax + ) + : IContractMetric + { + public ReadonlyContractMetric(ContractMetric metric) + : this(metric.Messages, metric.MessageBytes, metric.MessageBytesAverage, metric.MessageBytesMin, metric.MessageBytesMax, + metric.MessageConversionDuration, metric.MessageConversionAverage, metric.MessageConversionMin, metric.MessageConversionMax) + { } + } +} diff --git a/Core/Middleware/Metrics/SystemMetricTracker.cs b/Core/Middleware/Metrics/SystemMetricTracker.cs new file mode 100644 index 0000000..73b07b8 --- /dev/null +++ b/Core/Middleware/Metrics/SystemMetricTracker.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.Metrics; + +namespace MQContract.Middleware.Metrics +{ + internal class SystemMetricTracker + { + private const string MeterName = "mqcontract"; + + + private readonly SemaphoreSlim semDataLock = new(1, 1); + private readonly Meter meter; + private readonly MessageMetric globalMetric; + private readonly Dictionary typeMetrics = []; + private readonly Dictionary channelMetrics = []; + + public SystemMetricTracker(Meter meter) + { + this.meter = meter; + globalMetric = new( + meter.CreateUpDownCounter($"{MeterName}.messages.sent.count"), + meter.CreateUpDownCounter($"{MeterName}.messages.sent.bytes"), + meter.CreateUpDownCounter($"{MeterName}.messages.received.count"), + meter.CreateUpDownCounter($"{MeterName}.messages.received.bytes"), + meter.CreateHistogram($"{MeterName}.messages.encodingduration", unit: "ms"), + meter.CreateHistogram($"{MeterName}.messages.decodingduration", unit: "ms") + ); + } + + public void AppendEntry(MetricEntryValue entry) + { + globalMetric.AddEntry(entry); + semDataLock.Wait(); + MessageMetric? channelMetric = null; + if (!typeMetrics.TryGetValue(entry.Type, out MessageMetric? typeMetric)) + { + typeMetric = new( + meter.CreateUpDownCounter($"{MeterName}.types.{Utility.MessageTypeName(entry.Type)}.{Utility.MessageVersionString(entry.Type).Replace('.','_')}.sent.count"), + meter.CreateUpDownCounter($"{MeterName}.types.{Utility.MessageTypeName(entry.Type)}.{Utility.MessageVersionString(entry.Type).Replace('.', '_')}.sent.bytes"), + meter.CreateUpDownCounter($"{MeterName}.types.{Utility.MessageTypeName(entry.Type)}.{Utility.MessageVersionString(entry.Type).Replace('.', '_')}.received.count"), + meter.CreateUpDownCounter($"{MeterName}.types.{Utility.MessageTypeName(entry.Type)}.{Utility.MessageVersionString(entry.Type).Replace('.', '_')}.received.bytes"), + meter.CreateHistogram($"{MeterName}.types.{Utility.MessageTypeName(entry.Type)}.{Utility.MessageVersionString(entry.Type).Replace('.', '_')}.encodingduration", unit: "ms"), + meter.CreateHistogram($"{MeterName}.types.{Utility.MessageTypeName(entry.Type)}.{Utility.MessageVersionString(entry.Type).Replace('.', '_')}.decodingduration", unit: "ms") + ); + typeMetrics.Add(entry.Type, typeMetric!); + } + if (!string.IsNullOrWhiteSpace(entry.Channel) && !channelMetrics.TryGetValue(entry.Channel, out channelMetric)) + { + channelMetric = new( + meter.CreateUpDownCounter($"{MeterName}.channels.{entry.Channel}.sent.count"), + meter.CreateUpDownCounter($"{MeterName}.channels.{entry.Channel}.sent.bytes"), + meter.CreateUpDownCounter($"{MeterName}.channels.{entry.Channel}.received.count"), + meter.CreateUpDownCounter($"{MeterName}.channels.{entry.Channel}.received.bytes"), + meter.CreateHistogram($"{MeterName}.channels.{entry.Channel}.encodingduration", unit: "ms"), + meter.CreateHistogram($"{MeterName}.channels.{entry.Channel}.decodingduration", unit: "ms") + ); + channelMetrics.Add(entry.Channel!, channelMetric!); + } + typeMetric?.AddEntry(entry); + channelMetric?.AddEntry(entry); + semDataLock.Release(); + } + } +} diff --git a/Core/Middleware/MetricsMiddleware.cs b/Core/Middleware/MetricsMiddleware.cs new file mode 100644 index 0000000..2c67393 --- /dev/null +++ b/Core/Middleware/MetricsMiddleware.cs @@ -0,0 +1,94 @@ +using MQContract.Interfaces; +using MQContract.Interfaces.Middleware; +using MQContract.Messages; +using MQContract.Middleware.Metrics; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Threading.Channels; + +namespace MQContract.Middleware +{ + internal class MetricsMiddleware : IBeforeEncodeMiddleware, IAfterEncodeMiddleware,IBeforeDecodeMiddleware,IAfterDecodeMiddleware + { + private const string StopWatchKey = "_MetricStopwatch"; + private const string MessageReceivedChannelKey = "_MetricMessageReceivedChannel"; + private const string MessageReceivedSizeKey = "_MetricMessageReceivedSize"; + + private readonly SystemMetricTracker? systemTracker; + private readonly InternalMetricTracker? internalTracker; + private readonly Channel channel = Channel.CreateUnbounded(); + + public MetricsMiddleware(Meter? meter,bool useInternal) + { + if (meter!=null) + systemTracker=new(meter!); + if (useInternal) + internalTracker=new(); + Start(); + } + + private void Start() + { + Task.Run(async () => + { + while (await channel.Reader.WaitToReadAsync()) + { + var entry = await channel.Reader.ReadAsync(); + if (entry != null) + { + systemTracker?.AppendEntry(entry!); + internalTracker?.AppendEntry(entry!); + } + } + }); + } + + public IContractMetric? GetSnapshot(bool sent) + => internalTracker?.GetSnapshot(sent); + public IContractMetric? GetSnapshot(Type messageType, bool sent) + => internalTracker?.GetSnapshot(messageType, sent); + public IContractMetric? GetSnapshot(string channel, bool sent) + => internalTracker?.GetSnapshot(channel,sent); + + private async ValueTask AddStat(Type messageType, string? channel, bool sending, int messageSize, Stopwatch? stopWatch) + => await this.channel.Writer.WriteAsync(new(messageType, channel, sending, messageSize, stopWatch?.Elapsed??TimeSpan.Zero)); + + public async ValueTask<(T message, MessageHeader messageHeader)> AfterMessageDecodeAsync(IContext context, T message, string ID, MessageHeader messageHeader, DateTime receivedTimestamp, DateTime processedTimeStamp) + { + var stopWatch = (Stopwatch?)context[StopWatchKey]; + stopWatch?.Stop(); + await AddStat(typeof(T), (string?)context[MessageReceivedChannelKey]??string.Empty, false, (int?)context[MessageReceivedSizeKey]??0, stopWatch); + context[StopWatchKey]=null; + context[MessageReceivedChannelKey]=null; + context[MessageReceivedSizeKey]=null; + return (message,messageHeader); + } + + public async ValueTask AfterMessageEncodeAsync(Type messageType, IContext context, ServiceMessage message) + { + var stopWatch = (Stopwatch?)context[StopWatchKey]; + stopWatch?.Stop(); + await AddStat(messageType, message.Channel,true,message.Data.Length,stopWatch); + context[StopWatchKey] = null; + return message; + } + + public ValueTask<(MessageHeader messageHeader, ReadOnlyMemory data)> BeforeMessageDecodeAsync(IContext context, string id, MessageHeader messageHeader, string messageTypeID,string messageChannel, ReadOnlyMemory data) + { + context[MessageReceivedChannelKey] = messageChannel; + context[MessageReceivedSizeKey] = data.Length; + var stopwatch = new Stopwatch(); + context[StopWatchKey] = stopwatch; + stopwatch.Start(); + return ValueTask.FromResult((messageHeader, data)); + } + + public ValueTask<(T message, string? channel, MessageHeader messageHeader)> BeforeMessageEncodeAsync(IContext context, T message, string? channel, MessageHeader messageHeader) + { + var stopwatch = new Stopwatch(); + context[StopWatchKey] = stopwatch; + stopwatch.Start(); + return ValueTask.FromResult((message, channel, messageHeader)); + } + } +} diff --git a/Core/Readme.md b/Core/Readme.md index 92fe66c..5a76378 100644 --- a/Core/Readme.md +++ b/Core/Readme.md @@ -4,33 +4,35 @@ ## Contents - [ChannelMapper](#T-MQContract-ChannelMapper 'MQContract.ChannelMapper') - - [AddDefaultPublishMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultPublishMap-System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddDefaultPublishMap(System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddDefaultPublishSubscriptionMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultPublishSubscriptionMap-System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddDefaultPublishSubscriptionMap(System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddDefaultQueryMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultQueryMap-System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddDefaultQueryMap(System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddDefaultQuerySubscriptionMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultQuerySubscriptionMap-System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddDefaultQuerySubscriptionMap(System.Func{System.String,System.Threading.Tasks.Task{System.String}})') + - [AddDefaultPublishMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultPublishMap-System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddDefaultPublishMap(System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddDefaultPublishSubscriptionMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultPublishSubscriptionMap-System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddDefaultPublishSubscriptionMap(System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddDefaultQueryMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultQueryMap-System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddDefaultQueryMap(System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddDefaultQueryResponseMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultQueryResponseMap-System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddDefaultQueryResponseMap(System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddDefaultQuerySubscriptionMap(mapFunction)](#M-MQContract-ChannelMapper-AddDefaultQuerySubscriptionMap-System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddDefaultQuerySubscriptionMap(System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') - [AddPublishMap(originalChannel,newChannel)](#M-MQContract-ChannelMapper-AddPublishMap-System-String,System-String- 'MQContract.ChannelMapper.AddPublishMap(System.String,System.String)') - - [AddPublishMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddPublishMap-System-String,System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddPublishMap(System.String,System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddPublishMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddPublishMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddPublishMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.Task{System.String}})') + - [AddPublishMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddPublishMap-System-String,System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddPublishMap(System.String,System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddPublishMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddPublishMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddPublishMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') - [AddPublishSubscriptionMap(originalChannel,newChannel)](#M-MQContract-ChannelMapper-AddPublishSubscriptionMap-System-String,System-String- 'MQContract.ChannelMapper.AddPublishSubscriptionMap(System.String,System.String)') - - [AddPublishSubscriptionMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddPublishSubscriptionMap-System-String,System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddPublishSubscriptionMap(System.String,System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddPublishSubscriptionMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddPublishSubscriptionMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddPublishSubscriptionMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.Task{System.String}})') + - [AddPublishSubscriptionMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddPublishSubscriptionMap-System-String,System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddPublishSubscriptionMap(System.String,System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddPublishSubscriptionMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddPublishSubscriptionMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddPublishSubscriptionMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') - [AddQueryMap(originalChannel,newChannel)](#M-MQContract-ChannelMapper-AddQueryMap-System-String,System-String- 'MQContract.ChannelMapper.AddQueryMap(System.String,System.String)') - - [AddQueryMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddQueryMap-System-String,System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddQueryMap(System.String,System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddQueryMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddQueryMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddQueryMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.Task{System.String}})') + - [AddQueryMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddQueryMap-System-String,System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddQueryMap(System.String,System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddQueryMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddQueryMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddQueryMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddQueryResponseMap(originalChannel,newChannel)](#M-MQContract-ChannelMapper-AddQueryResponseMap-System-String,System-String- 'MQContract.ChannelMapper.AddQueryResponseMap(System.String,System.String)') + - [AddQueryResponseMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddQueryResponseMap-System-String,System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddQueryResponseMap(System.String,System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddQueryResponseMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddQueryResponseMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddQueryResponseMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') - [AddQuerySubscriptionMap(originalChannel,newChannel)](#M-MQContract-ChannelMapper-AddQuerySubscriptionMap-System-String,System-String- 'MQContract.ChannelMapper.AddQuerySubscriptionMap(System.String,System.String)') - - [AddQuerySubscriptionMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddQuerySubscriptionMap-System-String,System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddQuerySubscriptionMap(System.String,System.Func{System.String,System.Threading.Tasks.Task{System.String}})') - - [AddQuerySubscriptionMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddQuerySubscriptionMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-Task{System-String}}- 'MQContract.ChannelMapper.AddQuerySubscriptionMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.Task{System.String}})') + - [AddQuerySubscriptionMap(originalChannel,mapFunction)](#M-MQContract-ChannelMapper-AddQuerySubscriptionMap-System-String,System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddQuerySubscriptionMap(System.String,System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') + - [AddQuerySubscriptionMap(isMatch,mapFunction)](#M-MQContract-ChannelMapper-AddQuerySubscriptionMap-System-Func{System-String,System-Boolean},System-Func{System-String,System-Threading-Tasks-ValueTask{System-String}}- 'MQContract.ChannelMapper.AddQuerySubscriptionMap(System.Func{System.String,System.Boolean},System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}})') - [ContractConnection](#T-MQContract-ContractConnection 'MQContract.ContractConnection') - - [#ctor(serviceConnection,defaultMessageEncoder,defaultMessageEncryptor,serviceProvider,logger,channelMapper)](#M-MQContract-ContractConnection-#ctor-MQContract-Interfaces-Service-IMessageServiceConnection,MQContract-Interfaces-Encoding-IMessageEncoder,MQContract-Interfaces-Encrypting-IMessageEncryptor,System-IServiceProvider,Microsoft-Extensions-Logging-ILogger,MQContract-ChannelMapper- 'MQContract.ContractConnection.#ctor(MQContract.Interfaces.Service.IMessageServiceConnection,MQContract.Interfaces.Encoding.IMessageEncoder,MQContract.Interfaces.Encrypting.IMessageEncryptor,System.IServiceProvider,Microsoft.Extensions.Logging.ILogger,MQContract.ChannelMapper)') - - [PingAsync()](#M-MQContract-ContractConnection-PingAsync 'MQContract.ContractConnection.PingAsync') - - [PublishAsync\`\`1(message,channel,messageHeader,options,cancellationToken)](#M-MQContract-ContractConnection-PublishAsync``1-``0,System-String,MQContract-Messages-MessageHeader,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.ContractConnection.PublishAsync``1(``0,System.String,MQContract.Messages.MessageHeader,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync\`\`1(message,timeout,channel,messageHeader,options,cancellationToken)](#M-MQContract-ContractConnection-QueryAsync``1-``0,System-Nullable{System-TimeSpan},System-String,MQContract-Messages-MessageHeader,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.ContractConnection.QueryAsync``1(``0,System.Nullable{System.TimeSpan},System.String,MQContract.Messages.MessageHeader,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [QueryAsync\`\`2(message,timeout,channel,messageHeader,options,cancellationToken)](#M-MQContract-ContractConnection-QueryAsync``2-``0,System-Nullable{System-TimeSpan},System-String,MQContract-Messages-MessageHeader,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.ContractConnection.QueryAsync``2(``0,System.Nullable{System.TimeSpan},System.String,MQContract.Messages.MessageHeader,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeAsync\`\`1(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken)](#M-MQContract-ContractConnection-SubscribeAsync``1-System-Func{MQContract-Interfaces-IRecievedMessage{``0},System-Threading-Tasks-Task},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Boolean,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.ContractConnection.SubscribeAsync``1(System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Boolean,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') - - [SubscribeQueryResponseAsync\`\`2(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken)](#M-MQContract-ContractConnection-SubscribeQueryResponseAsync``2-System-Func{MQContract-Interfaces-IRecievedMessage{``0},System-Threading-Tasks-Task{MQContract-Messages-QueryResponseMessage{``1}}},System-Action{System-Exception},System-String,System-String,System-Boolean,System-Boolean,MQContract-Interfaces-Service-IServiceChannelOptions,System-Threading-CancellationToken- 'MQContract.ContractConnection.SubscribeQueryResponseAsync``2(System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task{MQContract.Messages.QueryResponseMessage{``1}}},System.Action{System.Exception},System.String,System.String,System.Boolean,System.Boolean,MQContract.Interfaces.Service.IServiceChannelOptions,System.Threading.CancellationToken)') + - [Instance(serviceConnection,defaultMessageEncoder,defaultMessageEncryptor,serviceProvider,logger,channelMapper)](#M-MQContract-ContractConnection-Instance-MQContract-Interfaces-Service-IMessageServiceConnection,MQContract-Interfaces-Encoding-IMessageEncoder,MQContract-Interfaces-Encrypting-IMessageEncryptor,System-IServiceProvider,Microsoft-Extensions-Logging-ILogger,MQContract-ChannelMapper- 'MQContract.ContractConnection.Instance(MQContract.Interfaces.Service.IMessageServiceConnection,MQContract.Interfaces.Encoding.IMessageEncoder,MQContract.Interfaces.Encrypting.IMessageEncryptor,System.IServiceProvider,Microsoft.Extensions.Logging.ILogger,MQContract.ChannelMapper)') +- [InvalidQueryResponseMessageReceived](#T-MQContract-InvalidQueryResponseMessageReceived 'MQContract.InvalidQueryResponseMessageReceived') - [MessageChannelNullException](#T-MQContract-MessageChannelNullException 'MQContract.MessageChannelNullException') - [MessageConversionException](#T-MQContract-MessageConversionException 'MQContract.MessageConversionException') +- [QueryExecutionFailedException](#T-MQContract-QueryExecutionFailedException 'MQContract.QueryExecutionFailedException') - [QueryResponseException](#T-MQContract-QueryResponseException 'MQContract.QueryResponseException') +- [QuerySubmissionFailedException](#T-MQContract-QuerySubmissionFailedException 'MQContract.QuerySubmissionFailedException') +- [QueryTimeoutException](#T-MQContract-QueryTimeoutException 'MQContract.QueryTimeoutException') - [SubscriptionFailedException](#T-MQContract-SubscriptionFailedException 'MQContract.SubscriptionFailedException') - [UnknownResponseTypeException](#T-MQContract-UnknownResponseTypeException 'MQContract.UnknownResponseTypeException') @@ -45,7 +47,7 @@ MQContract Used to map channel names depending on the usage of the channel when necessary - + ### AddDefaultPublishMap(mapFunction) `method` ##### Summary @@ -60,9 +62,9 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - + ### AddDefaultPublishSubscriptionMap(mapFunction) `method` ##### Summary @@ -77,9 +79,9 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - + ### AddDefaultQueryMap(mapFunction) `method` ##### Summary @@ -94,9 +96,26 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - + +### AddDefaultQueryResponseMap(mapFunction) `method` + +##### Summary + +Add a default map function to call for query/response response calls + +##### Returns + +The current instance of the Channel Mapper + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | + + ### AddDefaultQuerySubscriptionMap(mapFunction) `method` ##### Summary @@ -111,7 +130,7 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | ### AddPublishMap(originalChannel,newChannel) `method` @@ -131,7 +150,7 @@ The current instance of the Channel Mapper | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | | newChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel to map it to | - + ### AddPublishMap(originalChannel,mapFunction) `method` ##### Summary @@ -147,9 +166,9 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - + ### AddPublishMap(isMatch,mapFunction) `method` ##### Summary @@ -165,7 +184,7 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | isMatch | [System.Func{System.String,System.Boolean}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Boolean}') | A callback that will return true if the supplied function will mape that channel | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | ### AddPublishSubscriptionMap(originalChannel,newChannel) `method` @@ -185,7 +204,7 @@ The current instance of the Channel Mapper | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | | newChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel to map it to | - + ### AddPublishSubscriptionMap(originalChannel,mapFunction) `method` ##### Summary @@ -201,9 +220,9 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - + ### AddPublishSubscriptionMap(isMatch,mapFunction) `method` ##### Summary @@ -219,7 +238,7 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | isMatch | [System.Func{System.String,System.Boolean}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Boolean}') | A callback that will return true if the supplied function will mape that channel | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | ### AddQueryMap(originalChannel,newChannel) `method` @@ -239,7 +258,7 @@ The current instance of the Channel Mapper | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | | newChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel to map it to | - + ### AddQueryMap(originalChannel,mapFunction) `method` ##### Summary @@ -255,9 +274,9 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - + ### AddQueryMap(isMatch,mapFunction) `method` ##### Summary @@ -273,14 +292,14 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | isMatch | [System.Func{System.String,System.Boolean}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Boolean}') | A callback that will return true if the supplied function will mape that channel | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - -### AddQuerySubscriptionMap(originalChannel,newChannel) `method` + +### AddQueryResponseMap(originalChannel,newChannel) `method` ##### Summary -Add a direct map for query/response subscription calls +Add a direct map for query/response response calls ##### Returns @@ -293,12 +312,12 @@ The current instance of the Channel Mapper | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | | newChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel to map it to | - -### AddQuerySubscriptionMap(originalChannel,mapFunction) `method` + +### AddQueryResponseMap(originalChannel,mapFunction) `method` ##### Summary -Add a map function for query/response subscription calls +Add a map function for query/response response calls ##### Returns @@ -309,14 +328,14 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - -### AddQuerySubscriptionMap(isMatch,mapFunction) `method` + +### AddQueryResponseMap(isMatch,mapFunction) `method` ##### Summary -Add a map function call pair for query/response subscription calls +Add a map function call pair for query/response response calls ##### Returns @@ -327,224 +346,142 @@ The current instance of the Channel Mapper | Name | Type | Description | | ---- | ---- | ----------- | | isMatch | [System.Func{System.String,System.Boolean}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Boolean}') | A callback that will return true if the supplied function will mape that channel | -| mapFunction | [System.Func{System.String,System.Threading.Tasks.Task{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.Task{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - - -## ContractConnection `type` +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | -##### Namespace - -MQContract + +### AddQuerySubscriptionMap(originalChannel,newChannel) `method` ##### Summary -This is the primary class for this library and is used to create a Contract style connection between systems using the underlying service connection layer. - -##### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| serviceConnection | [T:MQContract.ContractConnection](#T-T-MQContract-ContractConnection 'T:MQContract.ContractConnection') | The service connection implementation to use for the underlying message requests. | - - -### #ctor(serviceConnection,defaultMessageEncoder,defaultMessageEncryptor,serviceProvider,logger,channelMapper) `constructor` +Add a direct map for query/response subscription calls -##### Summary +##### Returns -This is the primary class for this library and is used to create a Contract style connection between systems using the underlying service connection layer. +The current instance of the Channel Mapper ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| serviceConnection | [MQContract.Interfaces.Service.IMessageServiceConnection](#T-MQContract-Interfaces-Service-IMessageServiceConnection 'MQContract.Interfaces.Service.IMessageServiceConnection') | The service connection implementation to use for the underlying message requests. | -| defaultMessageEncoder | [MQContract.Interfaces.Encoding.IMessageEncoder](#T-MQContract-Interfaces-Encoding-IMessageEncoder 'MQContract.Interfaces.Encoding.IMessageEncoder') | A default message encoder implementation if desired. If there is no specific encoder for a given type, this encoder would be called. The built in default being used dotnet Json serializer. | -| defaultMessageEncryptor | [MQContract.Interfaces.Encrypting.IMessageEncryptor](#T-MQContract-Interfaces-Encrypting-IMessageEncryptor 'MQContract.Interfaces.Encrypting.IMessageEncryptor') | A default message encryptor implementation if desired. If there is no specific encryptor | -| serviceProvider | [System.IServiceProvider](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.IServiceProvider 'System.IServiceProvider') | A service prodivder instance supplied in the case that dependency injection might be necessary | -| logger | [Microsoft.Extensions.Logging.ILogger](#T-Microsoft-Extensions-Logging-ILogger 'Microsoft.Extensions.Logging.ILogger') | An instance of a logger if logging is desired | -| channelMapper | [MQContract.ChannelMapper](#T-MQContract-ChannelMapper 'MQContract.ChannelMapper') | An instance of a ChannelMapper used to translate channels from one instance to another based on class channel attributes or supplied channels if necessary. -For example, it might be necessary for a Nats.IO instance when you are trying to read from a stored message stream that is comprised of another channel or set of channels | - - -### PingAsync() `method` - -##### Summary - -Called to execute a ping against the service layer - -##### Returns - -The ping result from the service layer, if supported - -##### Parameters - -This method has no parameters. +| originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | +| newChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The channel to map it to | - -### PublishAsync\`\`1(message,channel,messageHeader,options,cancellationToken) `method` + +### AddQuerySubscriptionMap(originalChannel,mapFunction) `method` ##### Summary -Called to publish a message out into the service layer in the Pub/Sub style +Add a map function for query/response subscription calls ##### Returns -An instance of the TransmissionResult record to indicate success or failure and an ID +The current instance of the Channel Mapper ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| message | [\`\`0](#T-``0 '``0') | The instance of the message to publish | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to override the MessageChannelAttribute from the class or to specify a channel to transmit the message on | -| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | A message header to be sent across with the message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | - -##### Generic Types - -| Name | Description | -| ---- | ----------- | -| T | The type of message to publish | +| originalChannel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | The original channel that is being used in the connection | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | - -### QueryAsync\`\`1(message,timeout,channel,messageHeader,options,cancellationToken) `method` + +### AddQuerySubscriptionMap(isMatch,mapFunction) `method` ##### Summary -Called to publish a message in the Query/Response style except the response Type is gathered from the QueryResponseTypeAttribute +Add a map function call pair for query/response subscription calls ##### Returns -A QueryResult that will contain the response message and or an error +The current instance of the Channel Mapper ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| message | [\`\`0](#T-``0 '``0') | The message to transmit for the query | -| timeout | [System.Nullable{System.TimeSpan}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Nullable 'System.Nullable{System.TimeSpan}') | The timeout to allow for waiting for a response | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to override the MessageChannelAttribute from the class or to specify a channel to transmit the message on | -| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | A message header to be sent across with the message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | +| isMatch | [System.Func{System.String,System.Boolean}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Boolean}') | A callback that will return true if the supplied function will mape that channel | +| mapFunction | [System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{System.String,System.Threading.Tasks.ValueTask{System.String}}') | A function to be called with the channel supplied expecting a mapped channel name | -##### Generic Types + +## ContractConnection `type` -| Name | Description | -| ---- | ----------- | -| Q | The type of message to transmit for the Query | +##### Namespace -##### Exceptions +MQContract -| Name | Description | -| ---- | ----------- | -| [MQContract.UnknownResponseTypeException](#T-MQContract-UnknownResponseTypeException 'MQContract.UnknownResponseTypeException') | Thrown when the supplied Query type does not have a QueryResponseTypeAttribute and therefore a response type cannot be determined | +##### Summary - -### QueryAsync\`\`2(message,timeout,channel,messageHeader,options,cancellationToken) `method` +The primary ContractConnection item which implements IContractConnection + + +### Instance(serviceConnection,defaultMessageEncoder,defaultMessageEncryptor,serviceProvider,logger,channelMapper) `method` ##### Summary -Called to publish a message in the Query/Response style +This is the call used to create an instance of a Contract Connection which will return the Interface ##### Returns -A QueryResult that will contain the response message and or an error +An instance of IContractConnection ##### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| message | [\`\`0](#T-``0 '``0') | The message to transmit for the query | -| timeout | [System.Nullable{System.TimeSpan}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Nullable 'System.Nullable{System.TimeSpan}') | The timeout to allow for waiting for a response | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to override the MessageChannelAttribute from the class or to specify a channel to transmit the message on | -| messageHeader | [MQContract.Messages.MessageHeader](#T-MQContract-Messages-MessageHeader 'MQContract.Messages.MessageHeader') | A message header to be sent across with the message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | +| serviceConnection | [MQContract.Interfaces.Service.IMessageServiceConnection](#T-MQContract-Interfaces-Service-IMessageServiceConnection 'MQContract.Interfaces.Service.IMessageServiceConnection') | The service connection implementation to use for the underlying message requests. | +| defaultMessageEncoder | [MQContract.Interfaces.Encoding.IMessageEncoder](#T-MQContract-Interfaces-Encoding-IMessageEncoder 'MQContract.Interfaces.Encoding.IMessageEncoder') | A default message encoder implementation if desired. If there is no specific encoder for a given type, this encoder would be called. The built in default being used dotnet Json serializer. | +| defaultMessageEncryptor | [MQContract.Interfaces.Encrypting.IMessageEncryptor](#T-MQContract-Interfaces-Encrypting-IMessageEncryptor 'MQContract.Interfaces.Encrypting.IMessageEncryptor') | A default message encryptor implementation if desired. If there is no specific encryptor | +| serviceProvider | [System.IServiceProvider](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.IServiceProvider 'System.IServiceProvider') | A service prodivder instance supplied in the case that dependency injection might be necessary | +| logger | [Microsoft.Extensions.Logging.ILogger](#T-Microsoft-Extensions-Logging-ILogger 'Microsoft.Extensions.Logging.ILogger') | An instance of a logger if logging is desired | +| channelMapper | [MQContract.ChannelMapper](#T-MQContract-ChannelMapper 'MQContract.ChannelMapper') | An instance of a ChannelMapper used to translate channels from one instance to another based on class channel attributes or supplied channels if necessary. +For example, it might be necessary for a Nats.IO instance when you are trying to read from a stored message stream that is comprised of another channel or set of channels | -##### Generic Types + +## InvalidQueryResponseMessageReceived `type` -| Name | Description | -| ---- | ----------- | -| Q | The type of message to transmit for the Query | -| R | The type of message expected as a response | +##### Namespace - -### SubscribeAsync\`\`1(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken) `method` +MQContract ##### Summary -Called to establish a Subscription in the sevice layer for the Pub/Sub style messaging +Thrown when a query call message is received without proper data -##### Returns - -An instance of the Subscription that can be held or called to end + +## MessageChannelNullException `type` -##### Parameters +##### Namespace -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Interfaces.IRecievedMessage{\`\`0},System.Threading.Tasks.Task}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task}') | The callback to be executed when a message is recieved | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to be executed when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to override the MessageChannelAttribute from the class or to specify a channel to listen for messages on | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to specify a group to associate to at the service layer (refer to groups in KubeMQ, Nats.IO, etc) | -| ignoreMessageHeader | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | If set to true this will cause the subscription to ignore the message type specified and assume that the type of message is of type T | -| synchronous | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Set true if the desire the messageRecieved callback to be called such that it waits for the call to complete prior to calling for the next message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | +MQContract -##### Generic Types +##### Summary -| Name | Description | -| ---- | ----------- | -| T | The type of message to listen for | +Thrown when a call is made but the system is unable to detect the channel -##### Exceptions + +## MessageConversionException `type` -| Name | Description | -| ---- | ----------- | -| [MQContract.SubscriptionFailedException](#T-MQContract-SubscriptionFailedException 'MQContract.SubscriptionFailedException') | An exception thrown when the subscription has failed to establish | +##### Namespace - -### SubscribeQueryResponseAsync\`\`2(messageRecieved,errorRecieved,channel,group,ignoreMessageHeader,synchronous,options,cancellationToken) `method` +MQContract ##### Summary -Creates a subscription with the underlying service layer for the Query/Response style - -##### Returns - -An instance of the Subscription that can be held or called to end - -##### Parameters +Thrown when an incoming data message causes a null object return from a converter -| Name | Type | Description | -| ---- | ---- | ----------- | -| messageRecieved | [System.Func{MQContract.Interfaces.IRecievedMessage{\`\`0},System.Threading.Tasks.Task{MQContract.Messages.QueryResponseMessage{\`\`1}}}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Func 'System.Func{MQContract.Interfaces.IRecievedMessage{``0},System.Threading.Tasks.Task{MQContract.Messages.QueryResponseMessage{``1}}}') | The callback to be executed when a message is recieved and expects a returned response | -| errorRecieved | [System.Action{System.Exception}](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Action 'System.Action{System.Exception}') | The callback to be executed when an error occurs | -| channel | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to override the MessageChannelAttribute from the class or to specify a channel to listen for messages on | -| group | [System.String](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.String 'System.String') | Used to specify a group to associate to at the service layer (refer to groups in KubeMQ, Nats.IO, etc) | -| ignoreMessageHeader | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | If set to true this will cause the subscription to ignore the message type specified and assume that the type of message is of type T | -| synchronous | [System.Boolean](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Boolean 'System.Boolean') | Set true if the desire the messageRecieved callback to be called such that it waits for the call to complete prior to calling for the next message | -| options | [MQContract.Interfaces.Service.IServiceChannelOptions](#T-MQContract-Interfaces-Service-IServiceChannelOptions 'MQContract.Interfaces.Service.IServiceChannelOptions') | An instance of a ServiceChannelOptions to pass down to the service layer if desired and/or necessary | -| cancellationToken | [System.Threading.CancellationToken](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Threading.CancellationToken 'System.Threading.CancellationToken') | A cancellation token | + +## QueryExecutionFailedException `type` -##### Generic Types +##### Namespace -| Name | Description | -| ---- | ----------- | -| Q | The expected message type for the Query | -| R | The expected message type for the Response | +MQContract -##### Exceptions +##### Summary -| Name | Description | -| ---- | ----------- | -| [MQContract.SubscriptionFailedException](#T-MQContract-SubscriptionFailedException 'MQContract.SubscriptionFailedException') | An exception thrown when the subscription has failed to establish | +Thrown when a query call is being made to a service that does not support query response and the listener cannot be created - -## MessageChannelNullException `type` + +## QueryResponseException `type` ##### Namespace @@ -552,10 +489,10 @@ MQContract ##### Summary -Thrown when a call is made but the system is unable to detect the channel +Thrown when a Query call is made and there is an error in the response - -## MessageConversionException `type` + +## QuerySubmissionFailedException `type` ##### Namespace @@ -563,10 +500,10 @@ MQContract ##### Summary -Thrown when an incoming data message causes a null object return from a converter +Thrown when a query call is being made to an inbox style service and the message fails to transmit - -## QueryResponseException `type` + +## QueryTimeoutException `type` ##### Namespace @@ -574,7 +511,7 @@ MQContract ##### Summary -Thrown when a Query call is made and there is an error in the response +Thrown when a query call times out waiting for the response ## SubscriptionFailedException `type` diff --git a/Core/Subscriptions/PubSubSubscription.cs b/Core/Subscriptions/PubSubSubscription.cs index b1a02c8..e75c172 100644 --- a/Core/Subscriptions/PubSubSubscription.cs +++ b/Core/Subscriptions/PubSubSubscription.cs @@ -1,70 +1,42 @@ using Microsoft.Extensions.Logging; -using MQContract.Interfaces; -using MQContract.Interfaces.Factories; using MQContract.Interfaces.Service; using MQContract.Messages; -using System.Threading.Channels; namespace MQContract.Subscriptions { - internal sealed class PubSubSubscription(IMessageFactory messageFactory, Func,Task> messageRecieved, Action errorRecieved, - Func> mapChannel, - string? channel = null, string? group = null, bool synchronous=false,IServiceChannelOptions? options = null,ILogger? logger=null) + internal sealed class PubSubSubscription(Func messageReceived, Action errorReceived, + Func> mapChannel, + string? channel = null, string? group = null, bool synchronous=false,ILogger? logger=null) : SubscriptionBase(mapChannel,channel,synchronous) where T : class { - private readonly Channel dataChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() + public async ValueTask EstablishSubscriptionAsync(IMessageServiceConnection connection,CancellationToken cancellationToken) { - SingleReader=true, - SingleWriter=true - }); - - public async Task EstablishSubscriptionAsync(IMessageServiceConnection connection, CancellationToken cancellationToken = new CancellationToken()) - { - SyncToken(cancellationToken); serviceSubscription = await connection.SubscribeAsync( - async serviceMessage=>await dataChannel.Writer.WriteAsync(serviceMessage,token.Token), - error=>errorRecieved(error), + async serviceMessage => await ProcessMessage(serviceMessage), + error => errorReceived(error), MessageChannel, - group??Guid.NewGuid().ToString(), - options:options, - cancellationToken:token.Token + group:group, + cancellationToken: cancellationToken ); if (serviceSubscription==null) return false; - EstablishReader(); return true; } - private void EstablishReader() + private async ValueTask ProcessMessage(ReceivedServiceMessage serviceMessage) { - Task.Run(async () => + try { - while (await dataChannel.Reader.WaitToReadAsync(token.Token)) - { - while (dataChannel.Reader.TryRead(out var message)) - { - var tsk = Task.Run(async () => - { - try - { - var taskMessage = messageFactory.ConvertMessage(logger, message) - ??throw new InvalidCastException($"Unable to convert incoming message {message.MessageTypeID} to {typeof(T).FullName}"); - await messageRecieved(new RecievedMessage(message.ID,taskMessage!,message.Header,message.RecievedTimestamp,DateTime.Now)); - } - catch (Exception e) - { - errorRecieved(e); - } - }); - if (Synchronous) - await tsk; - } - } - }); + var tsk = messageReceived(serviceMessage); + await tsk.ConfigureAwait(!Synchronous); + if (serviceMessage.Acknowledge!=null) + await serviceMessage.Acknowledge(); + } + catch (Exception e) + { + errorReceived(e); + } } - - protected override void InternalDispose() - =>dataChannel.Writer.Complete(); } } diff --git a/Core/Subscriptions/QueryResponseHelper.cs b/Core/Subscriptions/QueryResponseHelper.cs new file mode 100644 index 0000000..34cdc5a --- /dev/null +++ b/Core/Subscriptions/QueryResponseHelper.cs @@ -0,0 +1,79 @@ +using MQContract.Interfaces.Service; +using MQContract.Messages; + +namespace MQContract.Subscriptions +{ + internal static class QueryResponseHelper + { + private const string QUERY_IDENTIFIER_HEADER = "_QueryClientID"; + private const string REPLY_ID = "_QueryReplyID"; + private const string REPLY_CHANNEL_HEADER = "_QueryReplyChannel"; + private static readonly string[] REQUIRED_HEADERS = [QUERY_IDENTIFIER_HEADER, REPLY_ID, REPLY_CHANNEL_HEADER]; + + public static MessageHeader StripHeaders(ServiceMessage originalMessage,out Guid queryClientID,out Guid replyID,out string? replyChannel) + { + queryClientID = new(originalMessage.Header[QUERY_IDENTIFIER_HEADER]!); + replyID = new(originalMessage.Header[REPLY_ID]!); + replyChannel = originalMessage.Header[REPLY_CHANNEL_HEADER]; + return new(originalMessage.Header.Keys + .Where(key=>!Equals(key,QUERY_IDENTIFIER_HEADER) + && !Equals(key,REPLY_ID) + && !Equals(key,REPLY_CHANNEL_HEADER) + ).Select(key =>new KeyValuePair(key,originalMessage.Header[key]!))); + } + + public static ServiceMessage EncodeMessage(ServiceMessage originalMessage, Guid queryClientID, Guid replyID,string? replyChannel,string? channel) + => new( + originalMessage.ID, + originalMessage.MessageTypeID, + channel??originalMessage.Channel, + new(originalMessage.Header,new Dictionary([ + new KeyValuePair(QUERY_IDENTIFIER_HEADER,queryClientID.ToString()), + new KeyValuePair(REPLY_ID,replyID.ToString()), + new KeyValuePair(REPLY_CHANNEL_HEADER,replyChannel) + ])), + originalMessage.Data + ); + + public static bool IsValidMessage(ReceivedServiceMessage serviceMessage) + => Array.TrueForAll(REQUIRED_HEADERS,key=>serviceMessage.Header.Keys.Contains(key)); + + public static async Task, CancellationTokenSource>> StartResponseListenerAsync(IMessageServiceConnection connection,TimeSpan timeout,Guid identifier,Guid callID,string replyChannel,CancellationToken cancellationToken) + { + var token = new CancellationTokenSource(); + var reg = cancellationToken.Register(() => token.Cancel()); + var result = new TaskCompletionSource(); + var consumer = await connection.SubscribeAsync( + async (message) => + { + if (!result.Task.IsCompleted) + { + var headers = StripHeaders(message, out var queryClientID, out var replyID, out _); + if (Equals(queryClientID, identifier) && Equals(replyID, callID)) + { + if (message.Acknowledge!=null) + await message.Acknowledge(); + result.TrySetResult(new( + message.ID, + headers, + message.MessageTypeID, + message.Data + )); + } + } + }, + error => { }, + replyChannel, + cancellationToken: token.Token + )??throw new QueryExecutionFailedException(); + token.Token.Register(async () => { + await consumer.EndAsync(); + await reg.DisposeAsync(); + if (!result.Task.IsCompleted) + result.TrySetException(new QueryTimeoutException()); + }); + token.CancelAfter(timeout); + return new Tuple, CancellationTokenSource>(result,token); + } + } +} diff --git a/Core/Subscriptions/QueryResponseSubscription.cs b/Core/Subscriptions/QueryResponseSubscription.cs index a899c59..a23c76a 100644 --- a/Core/Subscriptions/QueryResponseSubscription.cs +++ b/Core/Subscriptions/QueryResponseSubscription.cs @@ -1,61 +1,94 @@ using Microsoft.Extensions.Logging; -using MQContract.Interfaces; -using MQContract.Interfaces.Factories; using MQContract.Interfaces.Service; using MQContract.Messages; namespace MQContract.Subscriptions { - internal sealed class QueryResponseSubscription(IMessageFactory queryMessageFactory,IMessageFactory responseMessageFactory, - Func, Task>> messageRecieved, Action errorRecieved, - Func> mapChannel, + internal sealed class QueryResponseSubscription( + Func> processMessage, + Action errorReceived, + Func> mapChannel, string? channel = null, string? group = null, - bool synchronous=false,IServiceChannelOptions? options = null,ILogger? logger=null) - : SubscriptionBase(mapChannel,channel,synchronous),ISubscription - where Q : class - where R : class + bool synchronous=false,ILogger? logger=null) + : SubscriptionBase(mapChannel,channel,synchronous) + where T : class { - private readonly ManualResetEventSlim manualResetEvent = new(true); + private ManualResetEventSlim? manualResetEvent = new(true); + private CancellationTokenSource? token = new(); - public async Task EstablishSubscriptionAsync(IMessageServiceConnection connection, CancellationToken cancellationToken = new CancellationToken()) + public async ValueTask EstablishSubscriptionAsync(IMessageServiceConnection connection, CancellationToken cancellationToken) { - SyncToken(cancellationToken); - serviceSubscription = await connection.SubscribeQueryAsync( - serviceMessage => ProcessServiceMessageAsync(serviceMessage), - error => errorRecieved(error), - MessageChannel, - group??Guid.NewGuid().ToString(), - options: options, - cancellationToken: token.Token - ); + if (connection is IQueryableMessageServiceConnection queryableMessageServiceConnection) + serviceSubscription = await queryableMessageServiceConnection.SubscribeQueryAsync( + serviceMessage => ProcessServiceMessageAsync(serviceMessage, string.Empty), + error => errorReceived(error), + MessageChannel, + group: group, + cancellationToken: cancellationToken + ); + else + { + serviceSubscription = await connection.SubscribeAsync( + async (serviceMessage) => + { + if (!QueryResponseHelper.IsValidMessage(serviceMessage)) + errorReceived(new InvalidQueryResponseMessageReceived()); + else + { + var result = await ProcessServiceMessageAsync( + new( + serviceMessage.ID, + serviceMessage.MessageTypeID, + serviceMessage.Channel, + QueryResponseHelper.StripHeaders(serviceMessage, out var queryClientID, out var replyID, out var replyChannel), + serviceMessage.Data + ), + replyChannel! + ); + await connection.PublishAsync(QueryResponseHelper.EncodeMessage(result, queryClientID, replyID, null, replyChannel), cancellationToken); + } + }, + error => errorReceived(error), + MessageChannel, + cancellationToken: cancellationToken + ); + } return serviceSubscription!=null; } - private async Task ProcessServiceMessageAsync(RecievedServiceMessage message) + private async ValueTask ProcessServiceMessageAsync(ReceivedServiceMessage message,string replyChannel) { - if (Synchronous) - manualResetEvent.Wait(cancellationToken:token.Token); + if (Synchronous&&!(token?.IsCancellationRequested??false)) + manualResetEvent!.Wait(cancellationToken:token!.Token); Exception? error = null; ServiceMessage? response = null; try { - var taskMessage = queryMessageFactory.ConvertMessage(logger, message) - ??throw new InvalidCastException($"Unable to convert incoming message {message.MessageTypeID} to {typeof(Q).FullName}"); - var result = await messageRecieved(new RecievedMessage(message.ID, taskMessage,message.Header,message.RecievedTimestamp,DateTime.Now)); - response = await responseMessageFactory.ConvertMessageAsync(result.Message, message.Channel, new MessageHeader(result.Headers)); + response = await processMessage(message,replyChannel); + if (message.Acknowledge!=null) + await message.Acknowledge(); }catch(Exception e) { - errorRecieved(e); + errorReceived(e); error=e; } if (Synchronous) - manualResetEvent.Set(); + manualResetEvent!.Set(); if (error!=null) - return ErrorServiceMessage.Produce(message.Channel,error); - return response??ErrorServiceMessage.Produce(message.Channel, new NullReferenceException()); + return ErrorServiceMessage.Produce(replyChannel,error); + return response??ErrorServiceMessage.Produce(replyChannel, new NullReferenceException()); } protected override void InternalDispose() - =>manualResetEvent.Dispose(); + { + if (token!=null) + { + token.Cancel(); + manualResetEvent?.Dispose(); + token.Dispose(); + token=null; + manualResetEvent=null; + } + } } } diff --git a/Core/Subscriptions/SubscriptionBase.cs b/Core/Subscriptions/SubscriptionBase.cs index fba7ec2..d9d70cf 100644 --- a/Core/Subscriptions/SubscriptionBase.cs +++ b/Core/Subscriptions/SubscriptionBase.cs @@ -10,44 +10,44 @@ internal abstract class SubscriptionBase : ISubscription where T : class { protected IServiceSubscription? serviceSubscription; - protected readonly CancellationTokenSource token = new(); private bool disposedValue; + protected string MessageChannel { get; private init; } protected bool Synchronous { get; private init; } - protected SubscriptionBase(Func> mapChannel, string? channel=null,bool synchronous = false){ + public Guid ID { get; private init; } + + protected SubscriptionBase(Func> mapChannel, string? channel=null,bool synchronous = false){ + ID = Guid.NewGuid(); var chan = channel??typeof(T).GetCustomAttribute(false)?.Name??throw new MessageChannelNullException(); Synchronous = synchronous; - var tsk = mapChannel(chan); + var tsk = mapChannel(chan).AsTask(); tsk.Wait(); MessageChannel=tsk.Result; } - protected void SyncToken(CancellationToken cancellationToken) - => cancellationToken.Register(() => EndAsync().Wait()); - - [ExcludeFromCodeCoverage(Justification ="Virtual function that is implemented elsewhere")] + [ExcludeFromCodeCoverage(Justification = "Virtual function that is implemented elsewhere")] protected virtual void InternalDispose() - { } + { } - public async Task EndAsync() + public async ValueTask EndAsync() { if (serviceSubscription!=null) + { + System.Diagnostics.Debug.WriteLine("Calling subscription end async..."); await serviceSubscription.EndAsync(); - await token.CancelAsync(); + System.Diagnostics.Debug.WriteLine("Subscription ended async"); + serviceSubscription=null; + } } - protected void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) { if (!disposedValue) { - if (disposing) - { - EndAsync().Wait(); - InternalDispose(); - serviceSubscription?.Dispose(); - token.Dispose(); - } + if (disposing && serviceSubscription is IDisposable disposable) + disposable.Dispose(); + InternalDispose(); disposedValue=true; } } @@ -58,5 +58,16 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + + public async ValueTask DisposeAsync() + { + if (serviceSubscription is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(true); + else if (serviceSubscription is IDisposable disposable) + disposable.Dispose(); + + Dispose(false); + GC.SuppressFinalize(this); + } } } diff --git a/Core/Utility.cs b/Core/Utility.cs index f20adb4..9b8f5e7 100644 --- a/Core/Utility.cs +++ b/Core/Utility.cs @@ -1,7 +1,16 @@ -namespace MQContract +using MQContract.Attributes; +using System.Reflection; + +namespace MQContract { internal static class Utility { + internal static string MessageTypeName() + => MessageTypeName(typeof(T)); + + internal static string MessageTypeName(Type messageType) + => messageType.GetCustomAttributes().Select(mn => mn.Value).FirstOrDefault(TypeName(messageType)); + internal static string TypeName() => TypeName(typeof(T)); @@ -12,5 +21,18 @@ internal static string TypeName(Type type) result=result[..result.IndexOf('`')]; return result; } + + internal static string MessageVersionString() + => MessageVersionString(typeof(T)); + + internal static string MessageVersionString(Type messageType) + =>messageType.GetCustomAttributes().Select(mc => mc.Version.ToString()).FirstOrDefault("0.0.0.0"); + + internal static async ValueTask InvokeMethodAsync(MethodInfo method,object container, object?[]? parameters) + { + var valueTask = method.Invoke(container, parameters)!; + await (Task)valueTask.GetType().GetMethod(nameof(ValueTask.AsTask))!.Invoke(valueTask, null)!; + return valueTask.GetType().GetProperty(nameof(ValueTask.Result))!.GetValue(valueTask); + } } } diff --git a/MQContract.sln b/MQContract.sln index c44657a..0239d11 100644 --- a/MQContract.sln +++ b/MQContract.sln @@ -25,7 +25,27 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NATSSample", "Samples\NATSS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kafka", "Connectors\Kafka\Kafka.csproj", "{E3CE4E3B-8500-4888-BBA5-0256A129C9F5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KafkaSample", "Samples\KafkaSample\KafkaSample.csproj", "{76FFF0EF-C7F4-4D07-9CE1-CA695037CC11}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KafkaSample", "Samples\KafkaSample\KafkaSample.csproj", "{76FFF0EF-C7F4-4D07-9CE1-CA695037CC11}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActiveMQ", "Connectors\ActiveMQ\ActiveMQ.csproj", "{3DF8097C-D24F-4AB9-98E3-A17607ECCE25}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActiveMQSample", "Samples\ActiveMQSample\ActiveMQSample.csproj", "{F734932E-2624-4ADC-8EBE-FCE579AF09D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQ", "Connectors\RabbitMQ\RabbitMQ.csproj", "{0B5C5567-6EA2-4DA9-9DB5-E775630F5655}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Redis", "Connectors\Redis\Redis.csproj", "{DF36557A-1F5B-4C45-8B4D-9B3C7EE5A541}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedisSample", "Samples\RedisSample\RedisSample.csproj", "{EA577C5C-036F-4647-80F1-03F0EFAF6081}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQSample", "Samples\RabbitMQSample\RabbitMQSample.csproj", "{0740514D-A6AB-41CC-9820-A619125C6C90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HiveMQ", "Connectors\HiveMQ\HiveMQ.csproj", "{CF37AF9A-199E-4974-B33F-D3B34CE52988}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HiveMQSample", "Samples\HiveMQSample\HiveMQSample.csproj", "{17F3294A-6A89-4BDD-86E3-629D3506D147}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InMemory", "Connectors\InMemory\InMemory.csproj", "{B3E315EF-6FD3-46A8-9D7B-36E3B8D14787}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InMemorySample", "Samples\InMemorySample\InMemorySample.csproj", "{144D43A8-7154-4530-A78F-68DF7C4685E9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -73,6 +93,46 @@ Global {76FFF0EF-C7F4-4D07-9CE1-CA695037CC11}.Debug|Any CPU.Build.0 = Debug|Any CPU {76FFF0EF-C7F4-4D07-9CE1-CA695037CC11}.Release|Any CPU.ActiveCfg = Release|Any CPU {76FFF0EF-C7F4-4D07-9CE1-CA695037CC11}.Release|Any CPU.Build.0 = Release|Any CPU + {3DF8097C-D24F-4AB9-98E3-A17607ECCE25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DF8097C-D24F-4AB9-98E3-A17607ECCE25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DF8097C-D24F-4AB9-98E3-A17607ECCE25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DF8097C-D24F-4AB9-98E3-A17607ECCE25}.Release|Any CPU.Build.0 = Release|Any CPU + {F734932E-2624-4ADC-8EBE-FCE579AF09D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F734932E-2624-4ADC-8EBE-FCE579AF09D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F734932E-2624-4ADC-8EBE-FCE579AF09D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F734932E-2624-4ADC-8EBE-FCE579AF09D9}.Release|Any CPU.Build.0 = Release|Any CPU + {0B5C5567-6EA2-4DA9-9DB5-E775630F5655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B5C5567-6EA2-4DA9-9DB5-E775630F5655}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B5C5567-6EA2-4DA9-9DB5-E775630F5655}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B5C5567-6EA2-4DA9-9DB5-E775630F5655}.Release|Any CPU.Build.0 = Release|Any CPU + {DF36557A-1F5B-4C45-8B4D-9B3C7EE5A541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF36557A-1F5B-4C45-8B4D-9B3C7EE5A541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF36557A-1F5B-4C45-8B4D-9B3C7EE5A541}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF36557A-1F5B-4C45-8B4D-9B3C7EE5A541}.Release|Any CPU.Build.0 = Release|Any CPU + {EA577C5C-036F-4647-80F1-03F0EFAF6081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA577C5C-036F-4647-80F1-03F0EFAF6081}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA577C5C-036F-4647-80F1-03F0EFAF6081}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA577C5C-036F-4647-80F1-03F0EFAF6081}.Release|Any CPU.Build.0 = Release|Any CPU + {0740514D-A6AB-41CC-9820-A619125C6C90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0740514D-A6AB-41CC-9820-A619125C6C90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0740514D-A6AB-41CC-9820-A619125C6C90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0740514D-A6AB-41CC-9820-A619125C6C90}.Release|Any CPU.Build.0 = Release|Any CPU + {CF37AF9A-199E-4974-B33F-D3B34CE52988}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF37AF9A-199E-4974-B33F-D3B34CE52988}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF37AF9A-199E-4974-B33F-D3B34CE52988}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF37AF9A-199E-4974-B33F-D3B34CE52988}.Release|Any CPU.Build.0 = Release|Any CPU + {17F3294A-6A89-4BDD-86E3-629D3506D147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17F3294A-6A89-4BDD-86E3-629D3506D147}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17F3294A-6A89-4BDD-86E3-629D3506D147}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17F3294A-6A89-4BDD-86E3-629D3506D147}.Release|Any CPU.Build.0 = Release|Any CPU + {B3E315EF-6FD3-46A8-9D7B-36E3B8D14787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3E315EF-6FD3-46A8-9D7B-36E3B8D14787}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3E315EF-6FD3-46A8-9D7B-36E3B8D14787}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3E315EF-6FD3-46A8-9D7B-36E3B8D14787}.Release|Any CPU.Build.0 = Release|Any CPU + {144D43A8-7154-4530-A78F-68DF7C4685E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {144D43A8-7154-4530-A78F-68DF7C4685E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {144D43A8-7154-4530-A78F-68DF7C4685E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {144D43A8-7154-4530-A78F-68DF7C4685E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -85,6 +145,16 @@ Global {5A27E00D-B132-4292-9D98-272A172F883A} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} {E3CE4E3B-8500-4888-BBA5-0256A129C9F5} = {FCAD12F9-6992-44D7-8E78-464181584E06} {76FFF0EF-C7F4-4D07-9CE1-CA695037CC11} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} + {3DF8097C-D24F-4AB9-98E3-A17607ECCE25} = {FCAD12F9-6992-44D7-8E78-464181584E06} + {F734932E-2624-4ADC-8EBE-FCE579AF09D9} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} + {0B5C5567-6EA2-4DA9-9DB5-E775630F5655} = {FCAD12F9-6992-44D7-8E78-464181584E06} + {DF36557A-1F5B-4C45-8B4D-9B3C7EE5A541} = {FCAD12F9-6992-44D7-8E78-464181584E06} + {EA577C5C-036F-4647-80F1-03F0EFAF6081} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} + {0740514D-A6AB-41CC-9820-A619125C6C90} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} + {CF37AF9A-199E-4974-B33F-D3B34CE52988} = {FCAD12F9-6992-44D7-8E78-464181584E06} + {17F3294A-6A89-4BDD-86E3-629D3506D147} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} + {B3E315EF-6FD3-46A8-9D7B-36E3B8D14787} = {FCAD12F9-6992-44D7-8E78-464181584E06} + {144D43A8-7154-4530-A78F-68DF7C4685E9} = {0DAB9C52-BDAF-44CD-B10A-B9E057105696} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F4431E3A-7CBE-469C-A7FE-0A48F6768E02} diff --git a/README.md b/README.md index a68ec60..8168b7e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ global level or on a per message type level through implementation of the approp * [Abstractions](/Abstractions/Readme.md) * [Core](/Core/Readme.md) * Connectors - * [KubeMQ](/Connectors/KubeMQ/Readme.md) + * [ActiveMQ](/Connectors/ActiveMQ/Readme.md) + * [HiveMQ](/Connectors/HiveMQ/Readme.md) + * [InMemory](/Connectors/InMemory/Readme.md) * [Kafka](/Connectors/Kafka/Readme.md) + * [KubeMQ](/Connectors/KubeMQ/Readme.md) * [Nats.io](/Connectors/NATS/Readme.md) + * [RabbitMQ](/Connectors/RabbitMQ/Readme.md) + * [Redis](/Connectors/Redis/Readme.md) \ No newline at end of file diff --git a/Samples/ActiveMQSample/ActiveMQSample.csproj b/Samples/ActiveMQSample/ActiveMQSample.csproj new file mode 100644 index 0000000..3b86aa8 --- /dev/null +++ b/Samples/ActiveMQSample/ActiveMQSample.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/Samples/ActiveMQSample/Program.cs b/Samples/ActiveMQSample/Program.cs new file mode 100644 index 0000000..a4384d8 --- /dev/null +++ b/Samples/ActiveMQSample/Program.cs @@ -0,0 +1,6 @@ +using Messages; +using MQContract.ActiveMQ; + +var serviceConnection = new Connection(new Uri("amqp:tcp://localhost:5672"),"artemis","artemis"); + +await SampleExecution.ExecuteSample(serviceConnection, "ActiveMQ"); diff --git a/Samples/HiveMQSample/HiveMQSample.csproj b/Samples/HiveMQSample/HiveMQSample.csproj new file mode 100644 index 0000000..8bc0839 --- /dev/null +++ b/Samples/HiveMQSample/HiveMQSample.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/Samples/HiveMQSample/Program.cs b/Samples/HiveMQSample/Program.cs new file mode 100644 index 0000000..1a175b8 --- /dev/null +++ b/Samples/HiveMQSample/Program.cs @@ -0,0 +1,13 @@ +using HiveMQtt.Client.Options; +using Messages; +using MQContract.HiveMQ; + +var serviceConnection = new Connection(new HiveMQClientOptions +{ + Host = "127.0.0.1", + Port = 1883, + CleanStart = false, // <--- Set to false to receive messages queued on the broker + ClientId = "HiveMQSample" +}); + +await SampleExecution.ExecuteSample(serviceConnection, "HiveMQ"); diff --git a/Samples/InMemorySample/InMemorySample.csproj b/Samples/InMemorySample/InMemorySample.csproj new file mode 100644 index 0000000..3b2388f --- /dev/null +++ b/Samples/InMemorySample/InMemorySample.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/Samples/InMemorySample/Program.cs b/Samples/InMemorySample/Program.cs new file mode 100644 index 0000000..189cbbd --- /dev/null +++ b/Samples/InMemorySample/Program.cs @@ -0,0 +1,6 @@ +using Messages; +using MQContract.InMemory; + +var serviceConnection = new Connection(); + +await SampleExecution.ExecuteSample(serviceConnection, "InMemory"); \ No newline at end of file diff --git a/Samples/KafkaSample/Program.cs b/Samples/KafkaSample/Program.cs index 65a5b92..7e3808d 100644 --- a/Samples/KafkaSample/Program.cs +++ b/Samples/KafkaSample/Program.cs @@ -1,72 +1,10 @@ using Messages; -using MQContract; using MQContract.Kafka; -using MQContract.Kafka.Options; -using MQContract.Messages; -using var sourceCancel = new CancellationTokenSource(); - -Console.CancelKeyPress += delegate { - sourceCancel.Cancel(); -}; - -using var serviceConnection = new Connection(new Confluent.Kafka.ClientConfig() +var serviceConnection = new Connection(new Confluent.Kafka.ClientConfig() { ClientId="KafkaSample", BootstrapServers="localhost:56497" }); -var contractConnection = new ContractConnection(serviceConnection); - -using var arrivalSubscription = await contractConnection.SubscribeAsync( - (announcement) => - { - Console.WriteLine($"Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.RecievedTimestamp}]"); - return Task.CompletedTask; - }, - (error) => Console.WriteLine($"Announcement error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -using var greetingSubscription = await contractConnection.SubscribeQueryResponseAsync( - (greeting) => - { - Console.WriteLine($"Greeting recieved for {greeting.Message.LastName}, {greeting.Message.FirstName}. [{greeting.ID},{greeting.RecievedTimestamp}]"); - System.Diagnostics.Debug.WriteLine($"Time to convert message: {greeting.ProcessedTimestamp.Subtract(greeting.RecievedTimestamp).TotalMilliseconds}ms"); - return Task.FromResult>( - new($"Welcome {greeting.Message.FirstName} {greeting.Message.LastName} to the Kafka sample") - ); - }, - (error) => Console.WriteLine($"Greeting error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -using var storedArrivalSubscription = await contractConnection.SubscribeAsync( - (announcement) => - { - Console.WriteLine($"Stored Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.RecievedTimestamp}]"); - return Task.CompletedTask; - }, - (error) => Console.WriteLine($"Stored Announcement error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -var result = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Result 1 [Success:{!result.IsError}, ID:{result.ID}]"); -result = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Result 2 [Success:{!result.IsError}, ID:{result.ID}]"); - -var response = await contractConnection.QueryAsync(new Greeting("Bob", "Loblaw"), options:new QueryChannelOptions("Greeting.Response"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Response 1 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); -response = await contractConnection.QueryAsync(new Greeting("Fred", "Flintstone"), options: new QueryChannelOptions("Greeting.Response"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Response 2 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); - -var storedResult = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Stored Result 1 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); -storedResult = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Stored Result 2 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); - -Console.WriteLine("Press Ctrl+C to close"); - -sourceCancel.Token.WaitHandle.WaitOne(); -Console.WriteLine("System completed operation"); \ No newline at end of file +await SampleExecution.ExecuteSample(serviceConnection, "Kafka"); \ No newline at end of file diff --git a/Samples/KubeMQSample/Program.cs b/Samples/KubeMQSample/Program.cs index c03c8ef..7089f4b 100644 --- a/Samples/KubeMQSample/Program.cs +++ b/Samples/KubeMQSample/Program.cs @@ -1,73 +1,11 @@ using Messages; -using MQContract; using MQContract.KubeMQ; -using MQContract.KubeMQ.Options; -using MQContract.Messages; -using var sourceCancel = new CancellationTokenSource(); - -Console.CancelKeyPress += delegate { - sourceCancel.Cancel(); -}; - -using var serviceConnection = new Connection(new ConnectionOptions() +await using var serviceConnection = new Connection(new ConnectionOptions() { Logger=new Microsoft.Extensions.Logging.Debug.DebugLoggerProvider().CreateLogger("Messages"), ClientId="KubeMQSample" -}); - -var contractConnection = new ContractConnection(serviceConnection); - -using var arrivalSubscription = await contractConnection.SubscribeAsync( - (announcement) => - { - Console.WriteLine($"Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.RecievedTimestamp}]"); - return Task.CompletedTask; - }, - (error) => Console.WriteLine($"Announcement error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -using var greetingSubscription = await contractConnection.SubscribeQueryResponseAsync( - (greeting) => - { - Console.WriteLine($"Greeting recieved for {greeting.Message.LastName}, {greeting.Message.FirstName}. [{greeting.ID},{greeting.RecievedTimestamp}]"); - System.Diagnostics.Debug.WriteLine($"Time to convert message: {greeting.ProcessedTimestamp.Subtract(greeting.RecievedTimestamp).TotalMilliseconds}ms"); - return Task.FromResult>( - new($"Welcome {greeting.Message.FirstName} {greeting.Message.LastName} to the KubeMQ sample") - ); - }, - (error) => Console.WriteLine($"Greeting error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -using var storedArrivalSubscription = await contractConnection.SubscribeAsync( - (announcement) => - { - Console.WriteLine($"Stored Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.RecievedTimestamp}]"); - return Task.CompletedTask; - }, - (error) => Console.WriteLine($"Stored Announcement error: {error.Message}"), - options:new StoredEventsSubscriptionOptions(StoredEventsSubscriptionOptions.MessageReadStyle.StartNewOnly), - cancellationToken: sourceCancel.Token -); - -var result = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken:sourceCancel.Token); -Console.WriteLine($"Result 1 [Success:{!result.IsError}, ID:{result.ID}]"); -result = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Result 2 [Success:{!result.IsError}, ID:{result.ID}]"); - -var response = await contractConnection.QueryAsync(new Greeting("Bob","Loblaw"),cancellationToken:sourceCancel.Token); -Console.WriteLine($"Response 1 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); -response = await contractConnection.QueryAsync(new Greeting("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Response 2 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); - -var storedResult = await contractConnection.PublishAsync(new("Bob","Loblaw"),options:new PublishChannelOptions(true), cancellationToken:sourceCancel.Token); -Console.WriteLine($"Stored Result 1 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); -storedResult = await contractConnection.PublishAsync(new("Fred", "Flintstone"), options: new PublishChannelOptions(true), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Stored Result 2 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); - -Console.WriteLine("Press Ctrl+C to close"); +}) + .RegisterStoredChannel("StoredArrivals"); -sourceCancel.Token.WaitHandle.WaitOne(); -Console.WriteLine("System completed operation"); \ No newline at end of file +await SampleExecution.ExecuteSample(serviceConnection, "KubeMQ"); \ No newline at end of file diff --git a/Samples/Messages/Greeting.cs b/Samples/Messages/Greeting.cs index a8c6947..49f1b66 100644 --- a/Samples/Messages/Greeting.cs +++ b/Samples/Messages/Greeting.cs @@ -6,5 +6,6 @@ namespace Messages [MessageName("Nametag")] [MessageVersion("1.0.0.0")] [QueryResponseType(typeof(string))] + [QueryResponseChannel("Greeting.Response")] public record Greeting(string FirstName,string LastName){} } diff --git a/Samples/Messages/Messages.csproj b/Samples/Messages/Messages.csproj index 7ab567c..53fab42 100644 --- a/Samples/Messages/Messages.csproj +++ b/Samples/Messages/Messages.csproj @@ -8,6 +8,7 @@ + diff --git a/Samples/Messages/SampleExecution.cs b/Samples/Messages/SampleExecution.cs new file mode 100644 index 0000000..461ed16 --- /dev/null +++ b/Samples/Messages/SampleExecution.cs @@ -0,0 +1,97 @@ +using MQContract; +using MQContract.Interfaces; +using MQContract.Interfaces.Service; +using System.Text.Json; + +namespace Messages +{ + public static class SampleExecution + { + public static async ValueTask ExecuteSample(IMessageServiceConnection serviceConnection,string serviceName,ChannelMapper? mapper=null) + { + using var sourceCancel = new CancellationTokenSource(); + + var contractConnection = ContractConnection.Instance(serviceConnection,channelMapper:mapper); + contractConnection.AddMetrics(null, true); + + var announcementSubscription = await contractConnection.SubscribeAsync( + (announcement) => + { + Console.WriteLine($"Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.ReceivedTimestamp}]"); + return ValueTask.CompletedTask; + }, + (error) => Console.WriteLine($"Announcement error: {error.Message}"), + cancellationToken: sourceCancel.Token + ); + + var greetingSubscription = await contractConnection.SubscribeQueryResponseAsync( + (greeting) => + { + Console.WriteLine($"Greeting received for {greeting.Message.LastName}, {greeting.Message.FirstName}. [{greeting.ID},{greeting.ReceivedTimestamp}]"); + System.Diagnostics.Debug.WriteLine($"Time to convert message: {greeting.ProcessedTimestamp.Subtract(greeting.ReceivedTimestamp).TotalMilliseconds}ms"); + return new( + $"Welcome {greeting.Message.FirstName} {greeting.Message.LastName} to the {serviceName} sample" + ); + }, + (error) => Console.WriteLine($"Greeting error: {error.Message}"), + cancellationToken: sourceCancel.Token + ); + + var storedArrivalSubscription = await contractConnection.SubscribeAsync( + (announcement) => + { + Console.WriteLine($"Stored Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.ReceivedTimestamp}]"); + return ValueTask.CompletedTask; + }, + (error) => Console.WriteLine($"Stored Announcement error: {error.Message}"), + cancellationToken: sourceCancel.Token + ); + + sourceCancel.Token.Register(async () => + { + await Task.WhenAll( + announcementSubscription.EndAsync().AsTask(), + greetingSubscription.EndAsync().AsTask(), + storedArrivalSubscription.EndAsync().AsTask() + ).ConfigureAwait(true); + await contractConnection.CloseAsync().ConfigureAwait(true); + }, true); + + Console.WriteLine("Awaiting 5 seconds to ensure that all subscriptions are established fully."); + await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + Console.WriteLine("Beginning message transmissions..."); + + var result = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); + Console.WriteLine($"Result 1 [Success:{!result.IsError}, ID:{result.ID}]"); + result = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); + Console.WriteLine($"Result 2 [Success:{!result.IsError}, ID:{result.ID}]"); + + var response = await contractConnection.QueryAsync(new Greeting("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); + Console.WriteLine($"Response 1 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); + response = await contractConnection.QueryAsync(new Greeting("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); + Console.WriteLine($"Response 2 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); + + var storedResult = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); + Console.WriteLine($"Stored Result 1 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); + storedResult = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); + Console.WriteLine($"Stored Result 2 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); + + Console.WriteLine("Press Enter to close"); + + Console.ReadLine(); + await sourceCancel.CancelAsync(); + + Console.WriteLine("System completed operation"); + var jsonOptions = new JsonSerializerOptions() + { + WriteIndented = true + }; + Console.WriteLine($"Greetings Sent: {JsonSerializer.Serialize(contractConnection.GetSnapshot(typeof(Greeting), true), jsonOptions)}"); + Console.WriteLine($"Greetings Received: {JsonSerializer.Serialize(contractConnection.GetSnapshot(typeof(Greeting), false), jsonOptions)}"); + Console.WriteLine($"StoredArrivals Sent: {JsonSerializer.Serialize(contractConnection.GetSnapshot(typeof(ArrivalAnnouncement), true), jsonOptions)}"); + Console.WriteLine($"StoredArrivals Received: {JsonSerializer.Serialize(contractConnection.GetSnapshot(typeof(ArrivalAnnouncement), false), jsonOptions)}"); + Console.WriteLine($"Arrivals Sent: {JsonSerializer.Serialize(contractConnection.GetSnapshot(typeof(StoredArrivalAnnouncement), true), jsonOptions)}"); + Console.WriteLine($"Arrivals Received: {JsonSerializer.Serialize(contractConnection.GetSnapshot(typeof(StoredArrivalAnnouncement), false), jsonOptions)}"); + } + } +} diff --git a/Samples/NATSSample/Program.cs b/Samples/NATSSample/Program.cs index 8c96795..a569024 100644 --- a/Samples/NATSSample/Program.cs +++ b/Samples/NATSSample/Program.cs @@ -1,17 +1,9 @@ using Messages; using MQContract; -using MQContract.Messages; using MQContract.NATS; -using MQContract.NATS.Options; using NATS.Client.JetStream.Models; -using var sourceCancel = new CancellationTokenSource(); - -Console.CancelKeyPress += delegate { - sourceCancel.Cancel(); -}; - -using var serviceConnection = new Connection(new NATS.Client.Core.NatsOpts() +var serviceConnection = new Connection(new NATS.Client.Core.NatsOpts() { LoggerFactory=new Microsoft.Extensions.Logging.LoggerFactory(), Name="NATSSample" @@ -23,58 +15,4 @@ var mapper = new ChannelMapper() .AddPublishSubscriptionMap("StoredArrivals", "StoredArrivalsStream"); -var contractConnection = new ContractConnection(serviceConnection,channelMapper:mapper); - -using var arrivalSubscription = await contractConnection.SubscribeAsync( - (announcement) => - { - Console.WriteLine($"Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.RecievedTimestamp}]"); - return Task.CompletedTask; - }, - (error) => Console.WriteLine($"Announcement error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -using var greetingSubscription = await contractConnection.SubscribeQueryResponseAsync( - (greeting) => - { - Console.WriteLine($"Greeting recieved for {greeting.Message.LastName}, {greeting.Message.FirstName}. [{greeting.ID},{greeting.RecievedTimestamp}]"); - System.Diagnostics.Debug.WriteLine($"Time to convert message: {greeting.ProcessedTimestamp.Subtract(greeting.RecievedTimestamp).TotalMilliseconds}ms"); - return Task.FromResult>( - new($"Welcome {greeting.Message.FirstName} {greeting.Message.LastName} to the NATSio sample") - ); - }, - (error) => Console.WriteLine($"Greeting error: {error.Message}"), - cancellationToken: sourceCancel.Token -); - -using var storedArrivalSubscription = await contractConnection.SubscribeAsync( - (announcement) => - { - Console.WriteLine($"Stored Announcing the arrival of {announcement.Message.LastName}, {announcement.Message.FirstName}. [{announcement.ID},{announcement.RecievedTimestamp}]"); - return Task.CompletedTask; - }, - (error) => Console.WriteLine($"Stored Announcement error: {error.Message}"), - options:new StreamPublishSubscriberOptions(), - cancellationToken: sourceCancel.Token -); - -var result = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Result 1 [Success:{!result.IsError}, ID:{result.ID}]"); -result = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Result 2 [Success:{!result.IsError}, ID:{result.ID}]"); - -var response = await contractConnection.QueryAsync(new Greeting("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Response 1 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); -response = await contractConnection.QueryAsync(new Greeting("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Response 2 [Success:{!response.IsError}, ID:{response.ID}, Response: {response.Result}]"); - -var storedResult = await contractConnection.PublishAsync(new("Bob", "Loblaw"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Stored Result 1 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); -storedResult = await contractConnection.PublishAsync(new("Fred", "Flintstone"), cancellationToken: sourceCancel.Token); -Console.WriteLine($"Stored Result 2 [Success:{!storedResult.IsError}, ID:{storedResult.ID}]"); - -Console.WriteLine("Press Ctrl+C to close"); - -sourceCancel.Token.WaitHandle.WaitOne(); -Console.WriteLine("System completed operation"); \ No newline at end of file +await SampleExecution.ExecuteSample(serviceConnection, "NatsIO", mapper); \ No newline at end of file diff --git a/Samples/RabbitMQSample/Program.cs b/Samples/RabbitMQSample/Program.cs new file mode 100644 index 0000000..79671ea --- /dev/null +++ b/Samples/RabbitMQSample/Program.cs @@ -0,0 +1,19 @@ +using Messages; +using MQContract.RabbitMQ; +using RabbitMQ.Client; + +var factory = new ConnectionFactory() +{ + HostName = "localhost", + Port = 5672, + UserName="guest", + Password="guest", + MaxMessageSize=1024*1024*4 +}; + +var serviceConnection = new Connection(factory) + .ExchangeDeclare("Greeting", ExchangeType.Fanout) + .ExchangeDeclare("StoredArrivals", ExchangeType.Fanout,true) + .ExchangeDeclare("Arrivals", ExchangeType.Fanout); + +await SampleExecution.ExecuteSample(serviceConnection, "RabbitMQ"); \ No newline at end of file diff --git a/Samples/RabbitMQSample/RabbitMQSample.csproj b/Samples/RabbitMQSample/RabbitMQSample.csproj new file mode 100644 index 0000000..26ad5ce --- /dev/null +++ b/Samples/RabbitMQSample/RabbitMQSample.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/Samples/RedisSample/Program.cs b/Samples/RedisSample/Program.cs new file mode 100644 index 0000000..e19577d --- /dev/null +++ b/Samples/RedisSample/Program.cs @@ -0,0 +1,10 @@ +using Messages; +using MQContract.Redis; +using StackExchange.Redis; + +var conf = new ConfigurationOptions(); +conf.EndPoints.Add("localhost"); + +var serviceConnection = new Connection(conf); + +await SampleExecution.ExecuteSample(serviceConnection, "Redis"); diff --git a/Samples/RedisSample/RedisSample.csproj b/Samples/RedisSample/RedisSample.csproj new file mode 100644 index 0000000..74ec212 --- /dev/null +++ b/Samples/RedisSample/RedisSample.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Shared.props b/Shared.props new file mode 100644 index 0000000..3edeb33 --- /dev/null +++ b/Shared.props @@ -0,0 +1,21 @@ + + + 1.1 + https://github.com/roger-castaldo/MQContract + Readme.md + https://github.com/roger-castaldo/MQContract + $(VersionPrefix) + $(VersionPrefix) + Message Queue MQ Contract ServiceBus Messaging Abstraction PubSub QueryResponse + MIT + True + True + True + snupkg + true + $(MSBuildProjectDirectory)\Readme.md + True + roger-castaldo + $(AssemblyName) + + \ No newline at end of file