├── Testing ├── Core │ ├── GlobalUsings.cs │ ├── AssemblyInfo.cs │ ├── Messages │ │ ├── BasicResponseMessage.cs │ │ ├── NoChannelMessage.cs │ │ ├── BasicMessage.cs │ │ ├── CustomEncoderMessage.cs │ │ ├── CustomEncryptorMessage.cs │ │ ├── TimeoutMessage.cs │ │ ├── CustomEncoderWithInjectionMessage.cs │ │ ├── CustomEncryptorWithInjectionMessage.cs │ │ ├── NamedAndVersionedMessage.cs │ │ └── BasicQueryMessage.cs │ ├── ServiceInjection │ │ ├── InjectedService.cs │ │ └── IInjectableService.cs │ ├── ConnectionTests │ │ ├── Middlewares │ │ │ ├── InvalidMiddleware.cs │ │ │ ├── InjectedChannelChangeMiddleware.cs │ │ │ ├── ChannelChangeMiddleware.cs │ │ │ └── ChannelChangeMiddlewareForBasicMessage.cs │ │ └── SingleService │ │ │ └── PingTests.cs │ ├── Converters │ │ ├── NoChannelMessageToBasicMessage.cs │ │ └── BasicMessageToNameAndVersionMessage.cs │ ├── MoqExtensions.cs │ ├── Consumers │ │ ├── BasicQueryConsumer.cs │ │ ├── BasicMessageConsumer.cs │ │ ├── BasicMessageConsumerIgnoringMessageType.cs │ │ ├── BasicQueryAsyncConsumer.cs │ │ └── BasicMessageAsyncConsumer.cs │ ├── Encoders │ │ ├── TestMessageEncoder.cs │ │ └── TestMessageEncoderWithInjection.cs │ ├── Encryptors │ │ ├── TestMessageEncryptor.cs │ │ └── TestMessageEncryptorWithInjection.cs │ ├── Constants.cs │ ├── CoreTesting.csproj │ └── Helper.cs └── CQRSTesting │ ├── MSTestSettings.cs │ ├── Messages │ ├── BasicQueryResponse.cs │ ├── BasicCommandResponse.cs │ ├── BasicQuery.cs │ ├── BasicCommand.cs │ └── BasicResponseCommand.cs │ ├── Constants.cs │ ├── CQRSTesting.csproj │ └── ICQRSBaseTests.cs ├── images ├── performance.jpg └── open_telemetry.png ├── CQRS ├── CancellationRequest.cs ├── Interfaces │ ├── Query │ │ ├── IQuery.cs │ │ ├── IQueryInvocationContext.cs │ │ ├── IFilteredQueryProcessor.cs │ │ └── IQueryProcessor.cs │ ├── IProcessor.cs │ ├── Command │ │ ├── ICommand.cs │ │ ├── ICommandInvocationContext.cs │ │ ├── IFilteredCommandProcessor.cs │ │ └── ICommandProcessor.cs │ ├── IContextFilteredProcessor.cs │ └── IProcessorRegistrar.cs ├── CQRS.csproj ├── Contexts │ ├── QueryInvocationContext.cs │ └── CommandInvocationContext.cs ├── Attributes │ ├── CommandAttribute.cs │ └── QueryAttribute.cs ├── Consumers │ ├── CommandConsumer.cs │ ├── QueryResponseConsumer.cs │ ├── CommandResponseConsumer.cs │ └── CancellationRequestConsumer.cs ├── Extensions │ └── IContractConnectionExtension.cs ├── Registrars │ ├── MappedConnectionRegistrar.cs │ └── ContractedConnectionRegistrar.cs └── Exceptions.cs ├── BenchMark ├── Constants.cs ├── Messages │ ├── Announcement.cs │ ├── EncodedAnnouncement.cs │ └── AnnouncementCommand.cs ├── Encoders │ ├── MyJsonContext.cs │ └── EncodedAnnouncementEncoder.cs ├── BenchMark.csproj ├── InMemoryBenchmarks │ ├── AnnouncementConsumer.cs │ ├── AnnouncementCommandProcessor.cs │ └── SubscribingInMemory.cs ├── Program.cs └── PublishBenchmarks │ └── FakePublishConnection.cs ├── Samples ├── InMemorySample │ ├── Program.cs │ └── InMemorySample.csproj ├── Messages │ ├── ArrivalAnnouncement.cs │ ├── StoredArrivalAnnouncement.cs │ ├── Greeting.cs │ ├── Messages.csproj │ └── AnnouncementConsumer.cs ├── ActiveMQSample │ ├── Program.cs │ └── ActiveMQSample.csproj ├── RedisSample │ ├── Program.cs │ └── RedisSample.csproj ├── AzureServiceBusSample │ ├── Program.cs │ └── AzureServiceBusSample.csproj ├── KubeMQSample │ ├── Program.cs │ └── KubeMQSample.csproj ├── ZeroMQ │ ├── Program.cs │ └── ZeroMQ.csproj ├── HiveMQSample │ ├── Program.cs │ └── HiveMQSample.csproj ├── ApachePulsarSample │ ├── Program.cs │ └── ApachePulsarSample.csproj ├── AmazonSNQSSample │ └── AmazonSNQSSample.csproj ├── KafkaSample │ ├── KafkaSample.csproj │ └── Program.cs ├── GooglePubSubSample │ ├── GooglePubSubSample.csproj │ └── Program.cs ├── RabbitMQSample │ ├── RabbitMQSample.csproj │ └── Program.cs └── NATSSample │ ├── NATSSample.csproj │ └── Program.cs ├── Core ├── Interfaces │ ├── Factories │ │ ├── IMessageTypeFactory.cs │ │ └── IMessageFactory.cs │ └── Conversion │ │ └── IConversionPath.cs ├── Middleware │ ├── Metrics │ │ ├── MetricEntryValue.cs │ │ └── MessageMetric.cs │ ├── MiddlewareInjectionOrderAttribute.cs │ ├── ChannelMappingMiddleware.cs │ └── Context.cs ├── Connections │ ├── Records.cs │ └── DecodeServiceMessageResult.cs ├── Messages │ ├── RecievedMessage.cs │ └── ErrorServiceMessage.cs ├── Defaults │ ├── HalfEncoder.cs │ ├── IntEncoder.cs │ ├── BooleanEncoder.cs │ ├── UIntEncoder.cs │ ├── FloatEncoder.cs │ ├── LongEncoder.cs │ ├── ShortEncoder.cs │ ├── ULongEncoder.cs │ ├── DoubleEncoder.cs │ ├── UShortEncoder.cs │ ├── ByteEncoder.cs │ ├── BitConverterHelper.cs │ ├── ByteArrayEncoder.cs │ ├── CharEncoder.cs │ ├── JsonEncoder.cs │ ├── StringEncoder.cs │ └── DecimalEncoder.cs ├── EnumerableExtensions.cs ├── Core.csproj ├── Constants.cs ├── Factories │ └── AConverter.cs └── Subscriptions │ └── SubscriptionCollection.cs ├── Connectors ├── NATS │ ├── Options │ │ └── SubscriptionConsumerConfig.cs │ ├── Subscriptions │ │ ├── IInternalServiceSubscription.cs │ │ ├── PublishSubscription.cs │ │ ├── QuerySubscription.cs │ │ └── StreamSubscription.cs │ ├── NATS.csproj │ └── Exceptions.cs ├── KubeMQ │ ├── Options │ │ └── StoredChannelOptions.cs │ ├── Messages │ │ └── PingResponse.cs │ ├── KubeMQ.csproj │ ├── Utility.cs │ ├── Interfaces │ │ └── IKubeMQPingResult.cs │ ├── Exceptions.cs │ └── Subscriptions │ │ └── PubSubscription.cs ├── InMemory │ ├── Exceptions.cs │ ├── InternalServiceMessage.cs │ ├── InMemory.csproj │ ├── Subscription.cs │ ├── Readme.md │ └── MessageGroup.cs ├── HiveMQ │ ├── Exceptions.cs │ └── HiveMQ.csproj ├── AzureServiceBus │ ├── Exceptions.cs │ └── AzureServiceBus.csproj ├── ZeroMQ │ ├── ZeroMQ.csproj │ └── Exceptions.cs ├── Redis │ ├── Redis.csproj │ └── Subscriptions │ │ ├── PubSubscription.cs │ │ └── QueryResponseSubscription.cs ├── ActiveMQ │ ├── ActiveMQ.csproj │ ├── ConsumerInstance.cs │ ├── Subscriptions │ │ └── SubscriptionBase.cs │ └── Readme.md ├── ApachePulsar │ └── ApachePulsar.csproj ├── GooglePubSub │ ├── GooglePubSub.csproj │ └── Subscription.cs ├── AmazonSNQS │ └── AmazonSNQS.csproj ├── RabbitMQ │ ├── RabbitMQ.csproj │ └── Subscription.cs └── Kafka │ ├── Kafka.csproj │ └── Exceptions.cs ├── Abstractions ├── Interfaces │ ├── Middleware │ │ ├── IMiddleware.cs │ │ ├── ISpecificTypeMiddleware.cs │ │ ├── IContext.cs │ │ ├── IBeforeEncodeSpecificTypeMiddleware.cs │ │ ├── IBeforeEncodeMiddleware.cs │ │ ├── IAfterEncodeMiddleware.cs │ │ ├── IBeforeDecodeMiddleware.cs │ │ ├── IAfterDecodeSpecificTypeMiddleware.cs │ │ ├── IAfterDecodeMiddleware.cs │ │ └── Records.cs │ ├── IContractedConnection.cs │ ├── Encrypting │ │ ├── Records.cs │ │ ├── IMessageTypeEncryptor.cs │ │ └── IMessageEncryptor.cs │ ├── IMappedContractConnection.cs │ ├── Service │ │ ├── IServiceSubscription.cs │ │ ├── IPingableMessageServiceConnection.cs │ │ ├── IBulkPublishableMessageServiceConnection.cs │ │ ├── IQueryResponseMessageServiceConnection.cs │ │ ├── IQueryableMessageServiceConnection.cs │ │ └── IInboxQueryableMessageServiceConnection.cs │ ├── Consumers │ │ ├── IBaseConsumer.cs │ │ ├── IHeaderFilteredConsumer.cs │ │ ├── IPubSubConsumer.cs │ │ ├── IMessageFilteredConsumer.cs │ │ ├── IPubSubAsyncConsumer.cs │ │ ├── IQueryResponseConsumer.cs │ │ └── IQueryResponseAsyncConsumer.cs │ ├── ISubscription.cs │ ├── Messages │ │ └── IEncodedMessage.cs │ ├── Conversion │ │ └── IMessageConverter.cs │ ├── Encoding │ │ ├── IMessageTypeEncoder.cs │ │ └── IMessageEncoder.cs │ ├── IRecievedMessage.cs │ └── IContractMetric.cs ├── Abstractions.csproj ├── Messages │ ├── PingResult.cs │ ├── QueryResponseMessage.cs │ ├── TransmissionResult.cs │ ├── ServiceQueryResult.cs │ ├── QueryResult.cs │ ├── ServiceMessage.cs │ ├── MessageFilters.cs │ ├── MultiTransmissionResult.cs │ ├── RecievedInboxServiceMessage.cs │ └── RecievedServiceMessage.cs ├── Enums.cs └── Attributes │ ├── MessageAttribute.cs │ ├── ConsumerAttribute.cs │ └── QueryMessageAttribute.cs ├── OpenTelemetry.md ├── Resiliency.md ├── LICENSE ├── .github └── workflows │ ├── unit-test-report.yml │ └── unittests8x.yml ├── Clean-BuildFolders.ps1 └── Shared.props /Testing/Core/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using MQContract.Messages; 2 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/MSTestSettings.cs: -------------------------------------------------------------------------------- 1 | [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] 2 | -------------------------------------------------------------------------------- /Testing/Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: Parallelize(Workers = 25, Scope = ExecutionScope.ClassLevel)] -------------------------------------------------------------------------------- /images/performance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-castaldo/MQContract/HEAD/images/performance.jpg -------------------------------------------------------------------------------- /images/open_telemetry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-castaldo/MQContract/HEAD/images/open_telemetry.png -------------------------------------------------------------------------------- /CQRS/CancellationRequest.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS 2 | { 3 | internal record CancellationRequest(Guid CorrelationId,Guid MessageId) 4 | {} 5 | } 6 | -------------------------------------------------------------------------------- /Testing/Core/Messages/BasicResponseMessage.cs: -------------------------------------------------------------------------------- 1 | namespace AutomatedTesting.Messages 2 | { 3 | public record BasicResponseMessage(string TestName) { } 4 | } 5 | -------------------------------------------------------------------------------- /BenchMark/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace BenchMark 2 | { 3 | internal static class Constants 4 | { 5 | public const int PublishCount = 200; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/Messages/BasicQueryResponse.cs: -------------------------------------------------------------------------------- 1 | namespace CQRSTesting.Messages 2 | { 3 | public record BasicQueryResponse(string Name) 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Testing/Core/Messages/NoChannelMessage.cs: -------------------------------------------------------------------------------- 1 | namespace AutomatedTesting.Messages 2 | { 3 | public record NoChannelMessage(string TestName) 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/Messages/BasicCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace CQRSTesting.Messages 2 | { 3 | public record BasicCommandResponse(string Name) 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Testing/Core/ServiceInjection/InjectedService.cs: -------------------------------------------------------------------------------- 1 | namespace AutomatedTesting.ServiceInjection 2 | { 3 | internal record InjectedService(string Name) : IInjectableService 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Samples/InMemorySample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract.InMemory; 3 | 4 | var serviceConnection = new Connection(); 5 | 6 | await SampleExecution.ExecuteSample(serviceConnection, "InMemory"); -------------------------------------------------------------------------------- /Testing/Core/ServiceInjection/IInjectableService.cs: -------------------------------------------------------------------------------- 1 | namespace AutomatedTesting.ServiceInjection 2 | { 3 | internal interface IInjectableService 4 | { 5 | string Name { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Core/Interfaces/Factories/IMessageTypeFactory.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Factories 2 | { 3 | internal interface IMessageTypeFactory 4 | { 5 | bool IgnoreMessageHeader { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Testing/Core/Messages/BasicMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [Message(channel: "BasicMessage")] 6 | public record BasicMessage(string Name); 7 | } 8 | -------------------------------------------------------------------------------- /Core/Middleware/Metrics/MetricEntryValue.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Middleware.Metrics 2 | { 3 | internal record MetricEntryValue(Type Type, string? Channel, bool Sent, int MessageSize, TimeSpan Duration) 4 | { } 5 | } 6 | -------------------------------------------------------------------------------- /Samples/Messages/ArrivalAnnouncement.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace Messages 4 | { 5 | [Message(channel: "Arrivals")] 6 | public record ArrivalAnnouncement(string FirstName, string LastName) { } 7 | } 8 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Query/IQuery.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Query 2 | { 3 | /// 4 | /// Used to identify a query type 5 | /// 6 | public interface IQuery 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Testing/Core/Messages/CustomEncoderMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [Message(channel: "CustomEncoder")] 6 | public record CustomEncoderMessage(string TestName) { } 7 | } 8 | -------------------------------------------------------------------------------- /BenchMark/Messages/Announcement.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace BenchMark.Messages 4 | { 5 | [Message(typeName:"Announcement", typeVersion:"1.0.0")] 6 | public record Announcement(string Message) 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /Samples/Messages/StoredArrivalAnnouncement.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace Messages 4 | { 5 | [Message(channel: "StoredArrivals")] 6 | public record StoredArrivalAnnouncement(string FirstName, string LastName) { } 7 | } 8 | -------------------------------------------------------------------------------- /Testing/Core/ConnectionTests/Middlewares/InvalidMiddleware.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Middleware; 2 | 3 | namespace AutomatedTesting.ConnectionTests.Middlewares 4 | { 5 | internal class InvalidMiddleware : IMiddleware 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Testing/Core/Messages/CustomEncryptorMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [Message(channel: "CustomEncryptorMessage")] 6 | public record CustomEncryptorMessage(string TestName) { } 7 | } 8 | -------------------------------------------------------------------------------- /Testing/Core/Messages/TimeoutMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [QueryMessage(channel:"Timeout",responseTimeoutMilliseconds:500)] 6 | public record TimeoutMessage(string Name) { } 7 | } 8 | -------------------------------------------------------------------------------- /Connectors/NATS/Options/SubscriptionConsumerConfig.cs: -------------------------------------------------------------------------------- 1 | using NATS.Client.JetStream.Models; 2 | 3 | namespace MQContract.NATS.Options 4 | { 5 | internal record SubscriptionConsumerConfig(string Channel, ConsumerConfig Configuration) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Samples/ActiveMQSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract.ActiveMQ; 3 | 4 | var serviceConnection = new Connection(new Uri("amqp:tcp://localhost:5672"), "artemis", "artemis"); 5 | 6 | await SampleExecution.ExecuteSample(serviceConnection, "ActiveMQ"); 7 | -------------------------------------------------------------------------------- /Testing/Core/Messages/CustomEncoderWithInjectionMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [Message(channel: "CustomEncoderWithInjection")] 6 | public record CustomEncoderWithInjectionMessage(string TestName) { } 7 | } 8 | -------------------------------------------------------------------------------- /BenchMark/Encoders/MyJsonContext.cs: -------------------------------------------------------------------------------- 1 | using BenchMark.Messages; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace BenchMark.Encoders 5 | { 6 | [JsonSerializable(typeof(EncodedAnnouncement))] 7 | public partial class MyJsonContext : JsonSerializerContext { } 8 | } 9 | -------------------------------------------------------------------------------- /Testing/Core/Messages/CustomEncryptorWithInjectionMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [Message(channel: "CustomEncryptorWithInjection")] 6 | public record CustomEncryptorWithInjectionMessage(string TestName) { } 7 | } 8 | -------------------------------------------------------------------------------- /Core/Connections/Records.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using System.Diagnostics; 3 | 4 | namespace MQContract.Connections 5 | { 6 | internal readonly record struct FilteredServiceMessage(ServiceMessage? ServiceMessage, Activity? Activity, MessageFilterResult FilterResult); 7 | } 8 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/Messages/BasicQuery.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Attributes; 2 | using MQContract.CQRS.Interfaces.Query; 3 | 4 | namespace CQRSTesting.Messages 5 | { 6 | [Query("BasicQuery")] 7 | public record BasicQuery(string Name) : IQuery 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Connectors/NATS/Subscriptions/IInternalServiceSubscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Service; 2 | 3 | namespace MQContract.NATS.Subscriptions 4 | { 5 | internal interface IInternalServiceSubscription : IServiceSubscription 6 | { 7 | void Run(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// Base Middleware just used to limit Generic Types for Register Middleware 5 | /// 6 | public interface IMiddleware 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BenchMark/Messages/EncodedAnnouncement.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace BenchMark.Messages 4 | { 5 | [Message(typeName:"EncodedAnnouncement",typeVersion:"1.0.0")] 6 | public record EncodedAnnouncement(string Message) : Announcement(Message) 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/Messages/BasicCommand.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Attributes; 2 | using MQContract.CQRS.Interfaces.Command; 3 | 4 | namespace CQRSTesting.Messages 5 | { 6 | [Command("BasicCommand")] 7 | public record BasicCommand(string Name) : ICommand 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Testing/Core/Messages/NamedAndVersionedMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [Message(channel: "NamedAndVersioned",typeName:"VersionedMessage",typeVersion:"1.0.0.3")] 6 | public record NamedAndVersionedMessage(string TestName) { } 7 | } 8 | -------------------------------------------------------------------------------- /BenchMark/Messages/AnnouncementCommand.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Attributes; 2 | using MQContract.CQRS.Interfaces.Command; 3 | 4 | namespace BenchMark.Messages 5 | { 6 | [Command(channel:"Announcements")] 7 | public record AnnouncementCommand(string Message) : ICommand 8 | { } 9 | } 10 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/Options/StoredChannelOptions.cs: -------------------------------------------------------------------------------- 1 | using static MQContract.KubeMQ.Connection; 2 | 3 | namespace MQContract.KubeMQ.Options 4 | { 5 | internal record StoredChannelOptions(string ChannelName, MessageReadStyle ReadStyle = MessageReadStyle.StartNewOnly, long ReadOffset = 0) 6 | { } 7 | } 8 | -------------------------------------------------------------------------------- /Samples/Messages/Greeting.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace Messages 4 | { 5 | [QueryMessage(channel: "Greeting",typeName:"Nametag",typeVersion:"1.0.0.0",responseType:typeof(string),responseChannel:"Greeting.Response")] 6 | public record Greeting(string FirstName, string LastName) { } 7 | } 8 | -------------------------------------------------------------------------------- /Testing/Core/Messages/BasicQueryMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace AutomatedTesting.Messages 4 | { 5 | [QueryMessage(channel: "BasicQueryMessage",responseType:typeof(BasicResponseMessage),responseChannel: "BasicQueryResponse")] 6 | public record BasicQueryMessage(string TypeName) { } 7 | } 8 | -------------------------------------------------------------------------------- /Samples/RedisSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract.Redis; 3 | using StackExchange.Redis; 4 | 5 | var conf = new ConfigurationOptions(); 6 | conf.EndPoints.Add("localhost"); 7 | 8 | var serviceConnection = new Connection(conf); 9 | 10 | await SampleExecution.ExecuteSample(serviceConnection, "Redis"); 11 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/Messages/BasicResponseCommand.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Attributes; 2 | using MQContract.CQRS.Interfaces.Command; 3 | 4 | namespace CQRSTesting.Messages 5 | { 6 | [Command("BasicResponseCommand")] 7 | public record BasicResponseCommand(string Name) : ICommand 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Samples/AzureServiceBusSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract.AzureServiceBus; 3 | 4 | var serviceConnection = new Connection(new("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;")); 5 | 6 | await SampleExecution.ExecuteSample(serviceConnection, "AzureServiceBus"); -------------------------------------------------------------------------------- /Core/Messages/RecievedMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces; 2 | using System.Diagnostics; 3 | 4 | namespace MQContract.Messages 5 | { 6 | internal record ReceivedMessage(string ID, TMessage Message, MessageHeader Headers, DateTime ReceivedTimestamp, DateTime ProcessedTimestamp, Activity? Activity) 7 | : IReceivedMessage 8 | { } 9 | } 10 | -------------------------------------------------------------------------------- /Connectors/InMemory/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.InMemory 2 | { 3 | /// 4 | /// Thrown when a message transmission has failed within the In Memory system 5 | /// 6 | public class TransmissionResultException : Exception 7 | { 8 | internal TransmissionResultException() 9 | : base("Unable to transmit") { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Connectors/InMemory/InternalServiceMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.InMemory 4 | { 5 | internal record InternalServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, ReadOnlyMemory Data, Guid? CorrelationID = null, string? ReplyChannel = null) 6 | : ServiceMessage(ID, MessageTypeID, Channel, Header, Data) 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/IContractedConnection.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces 2 | { 3 | /// 4 | /// The base representation of a Contract Connection, specifically a single service connection supporting contract connection 5 | /// 6 | public interface IContractedConnection : IContractConnection, IMetricContractConnection 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Connectors/HiveMQ/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.HiveMQ 2 | { 3 | /// 4 | /// Thrown when the service connection is unable to connect to the HiveMQTT server 5 | /// 6 | public class ConnectionFailedException : Exception 7 | { 8 | internal ConnectionFailedException(string? reason) 9 | : base($"Failed to connect: {reason}") 10 | { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Core/Interfaces/Conversion/IConversionPath.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using MQContract.Interfaces.Messages; 3 | 4 | namespace MQContract.Interfaces.Conversion 5 | { 6 | internal interface IConversionPath 7 | { 8 | bool IsMatch(string metaData); 9 | ValueTask ConvertMessageAsync(ILogger? logger, IEncodedMessage message, Stream? dataStream = null); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Samples/KubeMQSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract.KubeMQ; 3 | 4 | await using var serviceConnection = new Connection(new ConnectionOptions() 5 | { 6 | Logger=new Microsoft.Extensions.Logging.Debug.DebugLoggerProvider().CreateLogger("Messages"), 7 | ClientId="KubeMQSample" 8 | }) 9 | .RegisterStoredChannel("StoredArrivals"); 10 | 11 | await SampleExecution.ExecuteSample(serviceConnection, "KubeMQ"); -------------------------------------------------------------------------------- /Testing/CQRSTesting/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace CQRSTesting 2 | { 3 | internal class Constants 4 | { 5 | public const string CorrelationIdTag = "mqcontract.cqrs.correlationid"; 6 | public const string MessageIdTag = "mqcontract.cqrs.messageid"; 7 | public const string CausationIdTag = "mqcontract.cqrs.causationid"; 8 | public const string TypeTag = "mqcontract.cqrs.type"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Connectors/AzureServiceBus/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.AzureServiceBus 2 | { 3 | /// 4 | /// Thrown when a bulk publish request exceeds the usable message batch size 5 | /// 6 | public class BulkTooLargeException : ArgumentException 7 | { 8 | internal BulkTooLargeException() 9 | : base("The bulk messages are too large for a batch.", "messages") { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Core/Defaults/HalfEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class HalfEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => 2; 6 | 7 | protected override Half ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToHalf(value); 9 | 10 | protected override byte[] ConvertValue(Half value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/IntEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class IntEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(int); 6 | 7 | protected override int ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToInt32(value); 9 | 10 | protected override byte[] ConvertValue(int value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/ZeroMQ/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | using Messages; 3 | using MQContract.ZeroMQ; 4 | 5 | var serviceConnection = new Connection(); 6 | serviceConnection.BindAsServer("tcp://localhost:8080"); 7 | serviceConnection.BindInboxAddress("tcp://localhost:8081"); 8 | serviceConnection.ConnectToServer("tcp://localhost:8080"); 9 | 10 | await SampleExecution.ExecuteSample(serviceConnection, "ZeroMQ"); -------------------------------------------------------------------------------- /CQRS/CQRS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.CQRS 7 | MQContract.CQRS 8 | CQRS wrapper for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Core/Defaults/BooleanEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class BooleanEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => 1; 6 | 7 | protected override bool ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToBoolean(value); 9 | 10 | protected override byte[] ConvertValue(bool value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/HiveMQSample/Program.cs: -------------------------------------------------------------------------------- 1 | using HiveMQtt.Client.Options; 2 | using Messages; 3 | using MQContract.HiveMQ; 4 | 5 | var serviceConnection = new Connection(new HiveMQClientOptions 6 | { 7 | Host = "127.0.0.1", 8 | Port = 1883, 9 | CleanStart = false, // <--- Set to false to receive messages queued on the broker 10 | ClientId = "HiveMQSample" 11 | }); 12 | 13 | await SampleExecution.ExecuteSample(serviceConnection, "HiveMQ"); 14 | -------------------------------------------------------------------------------- /Core/Defaults/UIntEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class UIntEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(uint); 6 | 7 | protected override uint ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToUInt32(value); 9 | 10 | protected override byte[] ConvertValue(uint value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/Messages/Messages.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Core/Defaults/FloatEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class FloatEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(float); 6 | 7 | protected override float ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToSingle(value); 9 | 10 | protected override byte[] ConvertValue(float value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/LongEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class LongEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(long); 6 | 7 | protected override long ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToInt64(value); 9 | 10 | protected override byte[] ConvertValue(long value) 11 | => BitConverter.GetBytes(value); 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Core/Defaults/ShortEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class ShortEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(short); 6 | 7 | protected override short ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToInt16(value); 9 | 10 | protected override byte[] ConvertValue(short value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/ULongEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class ULongEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(ulong); 6 | 7 | protected override ulong ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToUInt64(value); 9 | 10 | protected override byte[] ConvertValue(ulong value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/DoubleEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class DoubleEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(double); 6 | 7 | protected override double ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToDouble(value); 9 | 10 | protected override byte[] ConvertValue(double value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/UShortEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class UShortEncoder : ABitEncoder 4 | { 5 | protected override int ByteSize => sizeof(ushort); 6 | 7 | protected override ushort ConvertValue(ReadOnlySpan value) 8 | => BitConverter.ToUInt16(value); 9 | 10 | protected override byte[] ConvertValue(ushort value) 11 | => BitConverter.GetBytes(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Encrypting/Records.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Encrypting 2 | { 3 | /// 4 | /// Houses the returned results from a message encryption call 5 | /// 6 | /// Any additional headers to add to the message 7 | /// The resulting encrypted data 8 | public readonly record struct EncryptionResult(Dictionary? Headers, byte[] Data); 9 | } 10 | -------------------------------------------------------------------------------- /Testing/Core/Converters/NoChannelMessageToBasicMessage.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces.Conversion; 3 | 4 | namespace AutomatedTesting.Converters 5 | { 6 | internal class NoChannelMessageToBasicMessage : IMessageConverter 7 | { 8 | public ValueTask ConvertAsync(NoChannelMessage source) 9 | => ValueTask.FromResult(new(source.TestName)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/IMappedContractConnection.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces 2 | { 3 | /// 4 | /// The representation of a Mapped Contract Connection which is built to use 1 or more service connections for the calls 5 | /// 6 | public interface IMappedContractConnection 7 | : IContractConnection, IMetricContractConnection, IMappableContractConnection 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Core/Interfaces/Factories/IMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Conversion; 2 | using MQContract.Messages; 3 | 4 | namespace MQContract.Interfaces.Factories 5 | { 6 | internal interface IMessageFactory : IMessageTypeFactory, IConversionPath 7 | { 8 | string? MessageChannel { get; } 9 | ValueTask ConvertMessageAsync(TMessage message, bool ignoreChannel, string? channel, MessageHeader messageHeader); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/ISpecificTypeMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// Base Specific Type Middleware just used to limit Generic Types for Register Middleware 5 | /// 6 | #pragma warning disable S2326 // Unused type parameters should be removed 7 | public interface ISpecificTypeMiddleware 8 | #pragma warning restore S2326 // Unused type parameters should be removed 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Core/Defaults/ByteEncoder.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Encoding; 2 | 3 | namespace MQContract.Defaults 4 | { 5 | internal class ByteEncoder : IMessageTypeEncoder 6 | { 7 | ValueTask IMessageTypeEncoder.DecodeAsync(Stream stream) 8 | => ValueTask.FromResult((byte)stream.ReadByte()); 9 | 10 | ValueTask IMessageTypeEncoder.EncodeAsync(byte message) 11 | => ValueTask.FromResult([message]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/BitConverterHelper.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal static class BitConverterHelper 4 | { 5 | public static async ValueTask StreamToByteArray(Stream stream) 6 | { 7 | using var bufferedStream = new BufferedStream(stream); 8 | using var memoryStream = new MemoryStream(); 9 | await bufferedStream.CopyToAsync(memoryStream); 10 | return memoryStream.ToArray(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CQRS/Interfaces/IProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces 2 | { 3 | /// 4 | /// The base interface housing common calls for a Processor 5 | /// 6 | public interface IProcessor 7 | { 8 | /// 9 | /// Called when an error is supplied from the underlying Contract Connection 10 | /// 11 | /// The error that occured 12 | void ErrorRecieved(Exception error); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Testing/Core/Converters/BasicMessageToNameAndVersionMessage.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces.Conversion; 3 | 4 | namespace AutomatedTesting.Converters 5 | { 6 | internal class BasicMessageToNameAndVersionMessage : IMessageConverter 7 | { 8 | public ValueTask ConvertAsync(BasicMessage source) 9 | => ValueTask.FromResult(new(source.Name)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Service/IServiceSubscription.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Service 2 | { 3 | /// 4 | /// Represents an underlying service level subscription 5 | /// 6 | public interface IServiceSubscription 7 | { 8 | /// 9 | /// Called to end the subscription 10 | /// 11 | /// A task to allow for asynchronous ending of the subscription 12 | ValueTask EndAsync(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Core/Defaults/ByteArrayEncoder.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Encoding; 2 | 3 | namespace MQContract.Defaults 4 | { 5 | internal class ByteArrayEncoder : IMessageTypeEncoder 6 | { 7 | async ValueTask IMessageTypeEncoder.DecodeAsync(Stream stream) 8 | => await BitConverterHelper.StreamToByteArray(stream); 9 | 10 | ValueTask IMessageTypeEncoder.EncodeAsync(byte[] message) 11 | => ValueTask.FromResult(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Abstractions/Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract 8 | Abstractions for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /CQRS/Contexts/QueryInvocationContext.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Interfaces.Query; 2 | using MQContract.Interfaces; 3 | 4 | namespace MQContract.CQRS.Contexts 5 | { 6 | internal sealed class QueryInvocationContext(IReceivedMessage receivedMessage, CqrsConnection connection) 7 | : AInvocationContext(receivedMessage, connection), IQueryInvocationContext 8 | where TQuery : IQuery 9 | { 10 | TQuery IQueryInvocationContext.Query => Message; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Command/ICommand.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Command 2 | { 3 | /// 4 | /// Used to identify a command type 5 | /// 6 | public interface ICommand { } 7 | 8 | /// 9 | /// Used to identify a command type that is expected to provide a response 10 | /// 11 | /// The type of response expected from this command 12 | public interface ICommand : ICommand { } 13 | } 14 | -------------------------------------------------------------------------------- /Core/Defaults/CharEncoder.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Encoding; 2 | 3 | namespace MQContract.Defaults 4 | { 5 | internal class CharEncoder : IMessageTypeEncoder 6 | { 7 | async ValueTask IMessageTypeEncoder.DecodeAsync(Stream stream) 8 | => BitConverter.ToChar(await BitConverterHelper.StreamToByteArray(stream)); 9 | 10 | ValueTask IMessageTypeEncoder.EncodeAsync(char message) 11 | => ValueTask.FromResult(BitConverter.GetBytes(message)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/ZeroMQ/ZeroMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CQRS/Contexts/CommandInvocationContext.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Interfaces.Command; 2 | using MQContract.Interfaces; 3 | 4 | namespace MQContract.CQRS.Contexts 5 | { 6 | internal sealed class CommandInvocationContext(IReceivedMessage receivedMessage, CqrsConnection connection) 7 | : AInvocationContext(receivedMessage, connection), ICommandInvocationContext 8 | where TCommand : ICommand 9 | { 10 | TCommand ICommandInvocationContext.Command => Message; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Samples/ApachePulsarSample/Program.cs: -------------------------------------------------------------------------------- 1 | using DotPulsar; 2 | using Messages; 3 | using MQContract.ApachePulsar; 4 | 5 | #pragma warning disable S1075 // URIs should not be hardcoded 6 | //This is a sample program with a localhost connection so this is necessary 7 | var builder = PulsarClient.Builder() 8 | .ServiceUrl(new("pulsar://localhost:6650")); 9 | #pragma warning restore S1075 // URIs should not be hardcoded 10 | 11 | var serviceConnection = new Connection(builder!); 12 | 13 | await SampleExecution.ExecuteSample(serviceConnection, "ApachePulsar"); 14 | -------------------------------------------------------------------------------- /Samples/InMemorySample/InMemorySample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Abstractions/Messages/PingResult.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Messages 2 | { 3 | /// 4 | /// Houses the results from a Ping call against a given underlying service 5 | /// 6 | /// The host name of the service, if provided 7 | /// The version of the service running, if provided 8 | /// How long it took for the server to respond 9 | public record PingResult(string Host, string Version, TimeSpan ResponseTime) 10 | { } 11 | } 12 | -------------------------------------------------------------------------------- /Samples/AmazonSNQSSample/AmazonSNQSSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Samples/KafkaSample/KafkaSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Samples/HiveMQSample/HiveMQSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CQRS/Interfaces/IContextFilteredProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces 2 | { 3 | /// 4 | /// Used to define a processor that will filter incoming calls based on the context 5 | /// 6 | public interface IContextFilteredProcessor : IProcessor 7 | { 8 | /// 9 | /// The filter callback that will be supplied a context instance and will return a 10 | /// filter type response 11 | /// 12 | Func> Filter { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Samples/ActiveMQSample/ActiveMQSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Samples/ApachePulsarSample/ApachePulsarSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Samples/GooglePubSubSample/GooglePubSubSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Connectors/ZeroMQ/ZeroMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | ZeroMQ Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Query/IQueryInvocationContext.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Query 2 | { 3 | /// 4 | /// Represents a given execution context for a query 5 | /// 6 | /// The type of query housed within this context 7 | public interface IQueryInvocationContext : IInvocationContext 8 | where TQuery : IQuery 9 | { 10 | /// 11 | /// The query for this invocation context instance 12 | /// 13 | TQuery Query { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Samples/AzureServiceBusSample/AzureServiceBusSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IBaseConsumer.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Consumers 2 | { 3 | /// 4 | /// Represents the Base for all Consumer interfaces and contains the common method definition 5 | /// 6 | public interface IBaseConsumer 7 | { 8 | /// 9 | /// Called when an error is received from within the underlying subscription that is using this Consumer 10 | /// 11 | /// The error that occured 12 | void ErrorRecieved(Exception error); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/ISubscription.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces 2 | { 3 | /// 4 | /// This interface represents a Contract Connection Subscription and is used to house and end the subscription 5 | /// 6 | public interface ISubscription : IDisposable, IAsyncDisposable 7 | { 8 | /// 9 | /// Called to end (close off) the subscription 10 | /// 11 | /// A task that is ending the subscription and closing off the resources for it 12 | ValueTask EndAsync(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abstractions/Messages/QueryResponseMessage.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Messages 2 | { 3 | /// 4 | /// Houses the Query Response Message to be sent back from a query call 5 | /// 6 | /// The type of message contained in the response 7 | /// The message to respond back with 8 | /// The headers to attach to the response 9 | public record QueryResponseMessage(TQueryResponse Message, Dictionary? Headers = null); 10 | } 11 | -------------------------------------------------------------------------------- /Connectors/Redis/Redis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | Redis Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Connectors/NATS/NATS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | NATS.io Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Connectors/ActiveMQ/ActiveMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MQContract.$(MSBuildProjectName) 6 | MQContract.$(MSBuildProjectName) 7 | ActiveMQ Connector for MQContract 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Connectors/ApachePulsar/ApachePulsar.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | Apache Pulsar Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Command/ICommandInvocationContext.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Command 2 | { 3 | /// 4 | /// Represents a given execution context for a command 5 | /// 6 | /// The type of command housed within this context 7 | public interface ICommandInvocationContext : IInvocationContext 8 | where TCommand : ICommand 9 | { 10 | /// 11 | /// The command for this invocation context instance 12 | /// 13 | TCommand Command { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Connectors/GooglePubSub/GooglePubSub.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MQContract.$(MSBuildProjectName) 6 | MQContract.$(MSBuildProjectName) 7 | GooglePubSub Connector for MQContract 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Service/IPingableMessageServiceConnection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Service 4 | { 5 | /// 6 | /// Extends the base MessageServiceConnection Interface to support service pinging 7 | /// 8 | public interface IPingableMessageServiceConnection : IMessageServiceConnection 9 | { 10 | /// 11 | /// Implemented Ping call if avaialble for the underlying service 12 | /// 13 | /// A Ping Result 14 | ValueTask PingAsync(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IHeaderFilteredConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Consumers 4 | { 5 | /// 6 | /// Used to define a consumer that will filter out given messages using a header filter 7 | /// 8 | public interface IHeaderFilteredConsumer : IBaseConsumer 9 | { 10 | /// 11 | /// The filter callback to be invoked that will be supplied the current headers and expect back a filter type 12 | /// 13 | Func> Filter { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Connectors/InMemory/InMemory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | In Memory Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /BenchMark/BenchMark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Connectors/AzureServiceBus/AzureServiceBus.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MQContract.$(MSBuildProjectName) 6 | MQContract.$(MSBuildProjectName) 7 | AzureServiceBus Connector for MQContract 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Testing/Core/MoqExtensions.cs: -------------------------------------------------------------------------------- 1 | using Moq.Language.Flow; 2 | 3 | namespace AutomatedTesting 4 | { 5 | public static class MoqExtensions 6 | { 7 | public static IReturnsResult ReturnsInOrder( 8 | this ISetup setup, params TResult[] results) where T : class 9 | { 10 | var queue = new Queue(results); 11 | #pragma warning disable CS8603 // Possible null reference return. 12 | return setup.Returns(() => queue.Count > 0 ? queue.Dequeue() : default); 13 | #pragma warning restore CS8603 // Possible null reference return. 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Abstractions/Messages/TransmissionResult.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Messages 2 | { 3 | /// 4 | /// Houses the result of a transmission into the system 5 | /// 6 | /// The unique ID of the message that was transmitted 7 | /// An error message if an error occured 8 | public record TransmissionResult(string ID, ErrorMessage? Error = null) 9 | { 10 | /// 11 | /// Flag to indicate if the result is an error 12 | /// 13 | public bool IsError => !string.IsNullOrWhiteSpace(Error?.Message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Testing/Core/Consumers/BasicQueryConsumer.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces; 3 | using MQContract.Interfaces.Consumers; 4 | 5 | namespace AutomatedTesting.Consumers 6 | { 7 | internal class BasicQueryConsumer : IQueryResponseConsumer 8 | { 9 | void IBaseConsumer.ErrorRecieved(Exception error) 10 | { } 11 | 12 | QueryResponseMessage IQueryResponseConsumer.MessageReceived(IReceivedMessage message) 13 | => new(new(message.Message.TypeName)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /OpenTelemetry.md: -------------------------------------------------------------------------------- 1 | #OpenTelemetry 2 | 3 | To enable Open Telemetry support, simply call the EnableOpenTelemetry method: 4 | EnableOpenTelemetry(string activitySource = "MQContract", bool linkActivitiesAcrossSystems = true) 5 | Here you are able specify a custom activitySource is there is a desire, as well as indicate if the activities are to be linked across services. If this is enabled the system will pass across specific information within the service messages to tie the activities together. Below is an example screenshot from running a Query Response against a NATS service including linking the activities. 6 | 7 | ![Sample Query Response Output](images/open_telemetry.png) -------------------------------------------------------------------------------- /Samples/RedisSample/RedisSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Testing/Core/ConnectionTests/Middlewares/InjectedChannelChangeMiddleware.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.ServiceInjection; 2 | using MQContract.Interfaces.Middleware; 3 | 4 | namespace AutomatedTesting.ContractConnectionTests.Middlewares 5 | { 6 | internal class InjectedChannelChangeMiddleware(IInjectableService service) 7 | : IBeforeEncodeMiddleware 8 | { 9 | ValueTask> IBeforeEncodeMiddleware.BeforeMessageEncodeAsync(IContext context, EncodableMessage message) 10 | => ValueTask.FromResult>(new(message.MessageHeader, message.Message, service.Name)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Connectors/AmazonSNQS/AmazonSNQS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MQContract.$(MSBuildProjectName) 6 | MQContract.$(MSBuildProjectName) 7 | Amazon SNS/SQS Connector for MQContract 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Samples/RabbitMQSample/RabbitMQSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Connectors/ZeroMQ/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.ZeroMQ 2 | { 3 | /// 4 | /// Thrown when you have attempted to either create the inbox subscription or made a call to Query without setting up the inbox first 5 | /// 6 | public class UndefinedInboxException : Exception 7 | { 8 | internal UndefinedInboxException() 9 | : base("You must define the inbox connection address") { } 10 | 11 | internal static void ThrowIfNullOrWhiteSpace(string? inboxAddress) 12 | { 13 | if (string.IsNullOrWhiteSpace(inboxAddress)) 14 | throw new UndefinedInboxException(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Core/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract 2 | { 3 | internal static class EnumerableExtensions 4 | { 5 | public static async ValueTask WhenAll(this IEnumerable tasks) 6 | { 7 | foreach (var t in tasks) 8 | await t; 9 | } 10 | 11 | public static async ValueTask> WhenAll(this IEnumerable items, Func> callback) 12 | { 13 | IEnumerable result = []; 14 | foreach (var t in items) 15 | result = result.Append(await callback(t)); 16 | return result; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Testing/Core/ConnectionTests/Middlewares/ChannelChangeMiddleware.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Middleware; 2 | 3 | namespace AutomatedTesting.ContractConnectionTests.Middlewares 4 | { 5 | internal class ChannelChangeMiddleware : IBeforeEncodeMiddleware 6 | { 7 | public static string ChangeChannel(string? channel) 8 | => $"{channel}-Modified"; 9 | ValueTask> IBeforeEncodeMiddleware.BeforeMessageEncodeAsync(IContext context, EncodableMessage message) 10 | => ValueTask.FromResult>(new(message.MessageHeader, message.Message, ChangeChannel(message.Channel))); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Abstractions/Messages/ServiceQueryResult.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Messages; 2 | 3 | namespace MQContract.Messages 4 | { 5 | /// 6 | /// Houses a result from a query call from the Service Connection Level 7 | /// 8 | /// The ID of the message 9 | /// The headers transmitted 10 | /// The type of message encoded 11 | /// The encoded data of the message 12 | public record ServiceQueryResult(string ID, MessageHeader Header, string MessageTypeID, ReadOnlyMemory Data) 13 | : IEncodedMessage 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Samples/KubeMQSample/KubeMQSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Connectors/RabbitMQ/RabbitMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | RabbitMQ Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Samples/RabbitMQSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract.RabbitMQ; 3 | using RabbitMQ.Client; 4 | 5 | var factory = new ConnectionFactory() 6 | { 7 | HostName = "localhost", 8 | Port = 5672, 9 | UserName="guest", 10 | Password="guest", 11 | MaxInboundMessageBodySize=1024*1024*4 12 | }; 13 | 14 | var serviceConnection = new Connection(factory); 15 | await serviceConnection.ExchangeDeclareAsync("Greeting", ExchangeType.Fanout); 16 | await serviceConnection.ExchangeDeclareAsync("StoredArrivals", ExchangeType.Fanout, true); 17 | await serviceConnection.ExchangeDeclareAsync("Arrivals", ExchangeType.Fanout); 18 | 19 | await SampleExecution.ExecuteSample(serviceConnection, "RabbitMQ"); -------------------------------------------------------------------------------- /Testing/Core/Encoders/TestMessageEncoder.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces.Encoding; 3 | using System.Text; 4 | 5 | namespace AutomatedTesting.Encoders 6 | { 7 | internal class TestMessageEncoder : IMessageTypeEncoder 8 | { 9 | public ValueTask DecodeAsync(Stream stream) 10 | => ValueTask.FromResult(new CustomEncoderMessage(Encoding.ASCII.GetString(new BinaryReader(stream).ReadBytes((int)stream.Length)))); 11 | 12 | public ValueTask EncodeAsync(CustomEncoderMessage message) 13 | => ValueTask.FromResult(Encoding.ASCII.GetBytes(message.TestName)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Core/Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract 7 | MQContract 8 | Core for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Connectors/HiveMQ/HiveMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MQContract.$(MSBuildProjectName) 6 | MQContract.$(MSBuildProjectName) 7 | HiveMQ Connector for MQContract 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IPubSubConsumer.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Consumers 2 | { 3 | /// 4 | /// Represents a PubSub Message Consumer to be registered to the ContractConnection 5 | /// 6 | /// The type of Message that this Consumer will consume 7 | public interface IPubSubConsumer : IBaseConsumer 8 | { 9 | /// 10 | /// Called when a message is recieved from the underlying subscription that is using this Consumer 11 | /// 12 | /// The message that was received 13 | void MessageReceived(IReceivedMessage message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Connectors/Kafka/Kafka.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | Kafka Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Core/Messages/ErrorServiceMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace MQContract.Messages 4 | { 5 | internal static class ErrorServiceMessage 6 | { 7 | public const string MessageTypeID = "U-InternalServiceErrorMessage-1.0.0"; 8 | 9 | public static ServiceMessage Produce(string channel, Exception error) 10 | => new(Guid.NewGuid().ToString(), MessageTypeID, channel, new MessageHeader([]), EncodeError(error)); 11 | 12 | private static byte[] EncodeError(Exception error) 13 | => UTF8Encoding.UTF8.GetBytes(error.Message); 14 | 15 | public static QueryResponseException DecodeError(ReadOnlyMemory data) 16 | => new(UTF8Encoding.UTF8.GetString(data.ToArray())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Samples/Messages/AnnouncementConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces; 2 | using MQContract.Interfaces.Consumers; 3 | 4 | namespace Messages 5 | { 6 | internal class AnnouncementConsumer : IPubSubConsumer 7 | { 8 | void IBaseConsumer.ErrorRecieved(Exception error) 9 | { 10 | Console.WriteLine($"Announcement error: {error.Message}"); 11 | } 12 | 13 | void IPubSubConsumer.MessageReceived(IReceivedMessage message) 14 | { 15 | Console.WriteLine($"Announcing the arrival of {message.Message.LastName}, {message.Message.FirstName} in member 2 of the group.. [{message.ID},{message.ReceivedTimestamp}]"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/Messages/PingResponse.cs: -------------------------------------------------------------------------------- 1 | using MQContract.KubeMQ.Interfaces; 2 | using MQContract.Messages; 3 | 4 | namespace MQContract.KubeMQ.Messages 5 | { 6 | internal record PingResponse 7 | : PingResult, IKubeMQPingResult 8 | { 9 | private readonly MQContract.KubeMQ.SDK.Grpc.PingResult result; 10 | public PingResponse(MQContract.KubeMQ.SDK.Grpc.PingResult result, TimeSpan responseTime) 11 | : base(result.Host, result.Version, responseTime) 12 | { 13 | this.result=result; 14 | } 15 | public DateTime ServerStartTime => Utility.FromUnixTime(result.ServerStartTime); 16 | 17 | public TimeSpan ServerUpTime => TimeSpan.FromSeconds(result.ServerUpTimeSeconds); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BenchMark/InMemoryBenchmarks/AnnouncementConsumer.cs: -------------------------------------------------------------------------------- 1 | using BenchMark.Messages; 2 | using MQContract.Interfaces; 3 | using MQContract.Interfaces.Consumers; 4 | 5 | namespace BenchMark.InMemoryBenchmarks 6 | { 7 | internal class AnnouncementConsumer(int count, TaskCompletionSource completionSource) 8 | : IPubSubAsyncConsumer 9 | { 10 | void IBaseConsumer.ErrorRecieved(Exception error) 11 | { 12 | } 13 | 14 | ValueTask IPubSubAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 15 | { 16 | count--; 17 | if (count<=0) 18 | completionSource.TrySetResult(); 19 | return ValueTask.CompletedTask; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CQRS/Attributes/CommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace MQContract.CQRS.Attributes 4 | { 5 | /// 6 | /// Use this attribute to specify the Channel, TypeName and or TypeVersion of the 7 | /// Command being defined 8 | /// 9 | /// The channel to be used 10 | /// The command type to use 11 | /// The command type version to use 12 | [AttributeUsage(AttributeTargets.Class,AllowMultiple =false,Inherited =true)] 13 | public class CommandAttribute(string channel, string? typeName = null, string? typeVersion = null) 14 | : MessageAttribute(channel,typeName,typeVersion) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IMessageFilteredConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Consumers 4 | { 5 | /// 6 | /// Used to define a consumer that will filter out messages of a given message type 7 | /// 8 | /// The type of message the filter understands 9 | public interface IMessageFilteredConsumer : IBaseConsumer 10 | { 11 | /// 12 | /// Provides the filter callback that will be supplied the message headers and current message 13 | /// and expects back a filter instruction 14 | /// 15 | Func> Filter { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Abstractions/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract 2 | { 3 | /// 4 | /// These are the possible message filtering responses when supplying a message filtering action 5 | /// 6 | public enum MessageFilterResult 7 | { 8 | /// 9 | /// Allow the message to continue through 10 | /// 11 | Allow, 12 | /// 13 | /// Do not allow the message to continue to the callback and Acknowledge it within the service 14 | /// 15 | DropAndAcknowledge, 16 | /// 17 | /// Do not allow the message to continue to the callback and do not Acknowledge it within the service 18 | /// 19 | DropAndDontAcknowledge 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Command/IFilteredCommandProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Command 2 | { 3 | /// 4 | /// Used to define a command processor that will filter incoming messages based on the command 5 | /// 6 | /// The type of command that this processor handles 7 | public interface IFilteredCommandProcessor : ICommandProcessor 8 | where TCommand : ICommand 9 | { 10 | /// 11 | /// The filter callback expected to return a filter result and will be supplied the current 12 | /// context and command instance 13 | /// 14 | Func> Filter { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Abstractions/Messages/QueryResult.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Messages 2 | { 3 | /// 4 | /// Houses the result from a Query call into the system 5 | /// 6 | /// The type of message in the response 7 | /// The unique ID of the message 8 | /// The response headers 9 | /// The resulting response if there was one 10 | /// The error message for the response if it failed and an error was returned 11 | public record QueryResult(string ID, MessageHeader Header, TQueryResponse? Result = default, ErrorMessage? Error = null) 12 | : TransmissionResult(ID, Error) 13 | { } 14 | } 15 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Encrypting/IMessageTypeEncryptor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Encrypting 2 | { 3 | /// 4 | /// Used to define a specific message encryptor for the type T. 5 | /// This will override the global decryptor if specified for this connection 6 | /// as well as the default of not encrypting the message body 7 | /// 8 | /// The type of message that this encryptor supports 9 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S2326:Unused type parameters should be removed", Justification = "The generic type here is used to tag an encryptor specific to a message type")] 10 | public interface IMessageTypeEncryptor : IMessageEncryptor 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Messages/IEncodedMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Messages 4 | { 5 | /// 6 | /// Used to house an underlying message that has been encoded and is ready to be "shipped" into the underlying service layer 7 | /// 8 | public interface IEncodedMessage 9 | { 10 | /// 11 | /// The header for the given message 12 | /// 13 | MessageHeader Header { get; } 14 | /// 15 | /// The message type id to transmit across 16 | /// 17 | string MessageTypeID { get; } 18 | /// 19 | /// The encoded message 20 | /// 21 | ReadOnlyMemory Data { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Connectors/NATS/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.NATS 2 | { 3 | /// 4 | /// Thrown when an error occurs attempting to connect to the NATS server. 5 | /// Specifically this will be thrown when the Ping that is executed on each initial connection fails. 6 | /// 7 | public class UnableToConnectException : Exception 8 | { 9 | internal UnableToConnectException() 10 | : base("Unable to establish connection to the NATS host") { } 11 | } 12 | 13 | /// 14 | /// Thrown when a query response error is recieved through the system 15 | /// 16 | public class QueryAsyncReponseException : Exception 17 | { 18 | internal QueryAsyncReponseException(string error) 19 | : base(error) { } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Testing/Core/ConnectionTests/Middlewares/ChannelChangeMiddlewareForBasicMessage.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces.Middleware; 3 | 4 | namespace AutomatedTesting.ContractConnectionTests.Middlewares 5 | { 6 | internal class ChannelChangeMiddlewareForBasicMessage : IBeforeEncodeSpecificTypeMiddleware 7 | { 8 | public static string ChangeChannel(string? channel) 9 | => $"{channel}-ModifiedSpecifically"; 10 | ValueTask> IBeforeEncodeSpecificTypeMiddleware.BeforeMessageEncodeAsync(IContext context, EncodableMessage message) 11 | => ValueTask.FromResult>(new(message.MessageHeader, message.Message, ChangeChannel(message.Channel))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Testing/Core/Consumers/BasicMessageConsumer.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces; 3 | using MQContract.Interfaces.Consumers; 4 | 5 | namespace AutomatedTesting.Consumers 6 | { 7 | internal class BasicMessageConsumer : IPubSubConsumer 8 | { 9 | private static readonly List> messages = []; 10 | 11 | public static List> Messages => messages; 12 | 13 | public BasicMessageConsumer() 14 | { 15 | messages.Clear(); 16 | } 17 | 18 | void IBaseConsumer.ErrorRecieved(Exception error) 19 | { } 20 | 21 | void IPubSubConsumer.MessageReceived(IReceivedMessage message) 22 | => messages.Add(message); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BenchMark/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | using BenchMark.InMemoryBenchmarks; 3 | using BenchmarkDotNet.Configs; 4 | using BenchmarkDotNet.Jobs; 5 | using BenchmarkDotNet.Running; 6 | 7 | BenchmarkRunner.Run( 8 | typeof(SubscribingInMemory).Assembly, // all benchmarks from given assembly are going to be executed 9 | ManualConfig 10 | .Create( 11 | DefaultConfig.Instance 12 | .AddJob( 13 | BenchmarkDotNet.Jobs.Job.Default 14 | .WithWarmupCount(5) 15 | .WithMinIterationCount(3) 16 | .WithIterationCount(32) 17 | .WithInvocationCount(16) 18 | .WithMaxIterationCount(16) 19 | ) 20 | .WithOptions(ConfigOptions.DisableLogFile) 21 | ) 22 | ); -------------------------------------------------------------------------------- /Testing/Core/Consumers/BasicMessageConsumerIgnoringMessageType.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Attributes; 3 | using MQContract.Interfaces; 4 | using MQContract.Interfaces.Consumers; 5 | 6 | namespace AutomatedTesting.Consumers 7 | { 8 | [Consumer(ignoreMessageTypeHeader: true)] 9 | internal class BasicMessageConsumerIgnoringMessageType(List> messages, List errors) : IPubSubConsumer 10 | { 11 | public BasicMessageConsumerIgnoringMessageType() 12 | : this([], []) { } 13 | 14 | void IBaseConsumer.ErrorRecieved(Exception error) 15 | => errors.Add(error); 16 | 17 | void IPubSubConsumer.MessageReceived(IReceivedMessage message) 18 | => messages.Add(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IPubSubAsyncConsumer.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Consumers 2 | { 3 | /// 4 | /// Represents an Asynchronous PubSub Message Consumer to be registered to the ContractConnection 5 | /// 6 | /// The type of Message that this Consumer will consume 7 | public interface IPubSubAsyncConsumer : IBaseConsumer 8 | { 9 | /// 10 | /// Called when a message is recieved from the underlying subscription that is using this Consumer 11 | /// 12 | /// The message that was received 13 | /// A ValueTask for asynchronous operations 14 | ValueTask MessageReceivedAsync(IReceivedMessage message); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BenchMark/InMemoryBenchmarks/AnnouncementCommandProcessor.cs: -------------------------------------------------------------------------------- 1 | using BenchMark.Messages; 2 | using MQContract.CQRS.Interfaces; 3 | using MQContract.CQRS.Interfaces.Command; 4 | 5 | namespace BenchMark.InMemoryBenchmarks 6 | { 7 | internal class AnnouncementCommandProcessor(int count, TaskCompletionSource completionSource) : ICommandProcessor 8 | { 9 | void IProcessor.ErrorRecieved(Exception error) 10 | { 11 | } 12 | 13 | ValueTask ICommandProcessor.ProcessCommandAsync(ICommandInvocationContext invocationContext, CancellationToken cancellationToken) 14 | { 15 | count--; 16 | if (count<=0) 17 | completionSource.TrySetResult(); 18 | return ValueTask.CompletedTask; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Abstractions/Messages/ServiceMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Messages; 2 | 3 | namespace MQContract.Messages 4 | { 5 | /// 6 | /// Houses a service level message that would be supplied to the underlying Service Connection for transmission purposes 7 | /// 8 | /// The unique ID of the message 9 | /// An identifier that identifies the type of message encoded 10 | /// The channel to transmit the message on 11 | /// The headers to transmit with the message 12 | /// The content of the message 13 | public record ServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, ReadOnlyMemory Data) 14 | : IEncodedMessage 15 | { } 16 | } 17 | -------------------------------------------------------------------------------- /Core/Middleware/MiddlewareInjectionOrderAttribute.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Middleware; 2 | 3 | namespace MQContract.Middleware 4 | { 5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple=true,Inherited=false)] 6 | #pragma warning disable S2326 // Unused type parameters should be removed 7 | //T is required here as this attribute is used for defining the index in a specific manner for the given type of middleware 8 | internal class MiddlewareInjectionOrderAttribute(int preIndex=0,int postIndex=0) : Attribute 9 | #pragma warning restore S2326 // Unused type parameters should be removed 10 | where TMiddleware : IMiddleware 11 | { 12 | public int GetIndex(MiddlewareCollection.InjectionPositions position) 13 | => (position == MiddlewareCollection.InjectionPositions.Pre ? preIndex : postIndex); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Testing/Core/Consumers/BasicQueryAsyncConsumer.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Attributes; 3 | using MQContract.Interfaces; 4 | using MQContract.Interfaces.Consumers; 5 | 6 | namespace AutomatedTesting.Consumers 7 | { 8 | [Consumer(channel:"AsyncBasicQueryMessage",group:"AsyncBasicQueryMessageGroup")] 9 | internal class BasicQueryAsyncConsumer : IQueryResponseAsyncConsumer 10 | { 11 | void IBaseConsumer.ErrorRecieved(Exception error) 12 | { } 13 | 14 | ValueTask> IQueryResponseAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 15 | => ValueTask.FromResult>(new(new(message.Message.TypeName))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Core/Defaults/JsonEncoder.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Encoding; 2 | using System.Text.Json; 3 | 4 | namespace MQContract.Defaults 5 | { 6 | internal class JsonEncoder : IMessageTypeEncoder 7 | { 8 | private static JsonSerializerOptions JsonOptions => new() 9 | { 10 | WriteIndented=false, 11 | AllowTrailingCommas=true, 12 | PropertyNameCaseInsensitive=true, 13 | ReadCommentHandling=JsonCommentHandling.Skip 14 | }; 15 | 16 | public async ValueTask DecodeAsync(Stream stream) 17 | => await JsonSerializer.DeserializeAsync(stream, options: JsonOptions); 18 | 19 | public ValueTask EncodeAsync(TMessage message) 20 | => ValueTask.FromResult(JsonSerializer.SerializeToUtf8Bytes(message, JsonOptions)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Query/IFilteredQueryProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Query 2 | { 3 | /// 4 | /// Used to define a query processor that will filter incoming messages based on the query 5 | /// 6 | /// The type of query that this processor handles 7 | /// The type of response that this query processor provides 8 | public interface IFilteredQueryProcessor : IQueryProcessor 9 | where TQuery : IQuery 10 | { 11 | /// 12 | /// The filter callback expected to return a filter result and will be supplied the current 13 | /// context and query instance 14 | /// 15 | Func> Filter { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/KubeMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQContract.$(MSBuildProjectName) 7 | MQContract.$(MSBuildProjectName) 8 | KubeMQ Connector for MQContract 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Connectors/Redis/Subscriptions/PubSubscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using StackExchange.Redis; 3 | 4 | namespace MQContract.Redis.Subscriptions 5 | { 6 | internal class PubSubscription(Func messageReceived, Action errorReceived, IDatabase database, Guid connectionID, string channel, string? group) 7 | : SubscriptionBase(errorReceived, database, connectionID, channel, group) 8 | { 9 | protected override async ValueTask ProcessMessage(StreamEntry streamEntry, string channel, string? group) 10 | { 11 | (var message, _, _) = Connection.ConvertMessage( 12 | streamEntry.Values, 13 | channel, 14 | () => Acknowledge(streamEntry.Id) 15 | ); 16 | await messageReceived(message).ConfigureAwait(false); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Connectors/NATS/Subscriptions/PublishSubscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using NATS.Client.Core; 3 | 4 | namespace MQContract.NATS.Subscriptions 5 | { 6 | internal class PublishSubscription(IAsyncEnumerable> asyncEnumerable, 7 | Func messageReceived, Action errorReceived) 8 | : SubscriptionBase() 9 | { 10 | protected override async Task RunAction() 11 | { 12 | await foreach (var msg in asyncEnumerable.WithCancellation(CancelToken)) 13 | { 14 | try 15 | { 16 | await messageReceived(ExtractMessage(msg)).ConfigureAwait(false); 17 | } 18 | catch (Exception ex) 19 | { 20 | errorReceived(ex); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IContext.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace MQContract.Interfaces.Middleware 4 | { 5 | /// 6 | /// 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 7 | /// 8 | public interface IContext 9 | { 10 | /// 11 | /// Used to store and retreive values from the context during the conversion process. 12 | /// 13 | /// The unique key to use 14 | /// The value if it exists in the context 15 | object? this[string key] { get; set; } 16 | /// 17 | /// Houses the current activity (if set) for the given context for Open Telemetry usage 18 | /// 19 | Activity? Activity { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Core/Connections/DecodeServiceMessageResult.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Connections 4 | { 5 | internal record DecodeServiceMessageResult 6 | { 7 | public TMessage? Message { get; private init; } = default(TMessage?); 8 | public MessageHeader? Header { get; private init; } = null; 9 | public MessageFilterResult FilterResult { get; private init; } = MessageFilterResult.Allow; 10 | 11 | public static DecodeServiceMessageResult ProduceResult(TMessage message, MessageHeader messageHeader) 12 | => new() 13 | { 14 | Message = message, 15 | Header = messageHeader 16 | }; 17 | 18 | public static DecodeServiceMessageResult ProduceResult(MessageFilterResult messageFilterResult) 19 | => new() { FilterResult = messageFilterResult }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Core/Defaults/StringEncoder.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Encoding; 2 | using System.Text; 3 | 4 | namespace MQContract.Defaults 5 | { 6 | internal class StringEncoder : IMessageTypeEncoder 7 | { 8 | async ValueTask IMessageTypeEncoder.DecodeAsync(Stream stream) 9 | { 10 | using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 8192, leaveOpen: true); 11 | return await reader.ReadToEndAsync(); 12 | } 13 | 14 | ValueTask IMessageTypeEncoder.EncodeAsync(string message) 15 | { 16 | int byteCount = Encoding.UTF8.GetByteCount(message); 17 | byte[] bytes = new byte[byteCount]; 18 | Encoding.UTF8.GetBytes(message.AsSpan(), bytes.AsSpan()); 19 | return ValueTask.FromResult(bytes); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CQRS/Consumers/CommandConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Contexts; 2 | using MQContract.CQRS.Interfaces.Command; 3 | using MQContract.Interfaces; 4 | using MQContract.Interfaces.Consumers; 5 | 6 | namespace MQContract.CQRS.Consumers 7 | { 8 | internal class CommandConsumer(ICommandProcessor commandProcessor, CqrsConnection connection) : IPubSubAsyncConsumer 9 | where TCommand : ICommand 10 | { 11 | void IBaseConsumer.ErrorRecieved(Exception error) 12 | => commandProcessor.ErrorRecieved(error); 13 | 14 | async ValueTask IPubSubAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 15 | { 16 | await using var context = new CommandInvocationContext(message, connection); 17 | await commandProcessor.ProcessCommandAsync(context,context.CancellationTokenSource.Token); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Samples/NATSSample/NATSSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IQueryResponseConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Consumers 4 | { 5 | /// 6 | /// Represents a QueryResponse Message Consumer to be registered to the ContractConnection 7 | /// 8 | /// The type of Message that is received and will be consumed 9 | /// The type of Message that is returned as a response 10 | public interface IQueryResponseConsumer : IBaseConsumer 11 | { 12 | /// 13 | /// Called when a message is received from the underlying subscript that is using this Consumer 14 | /// 15 | /// The message that was received 16 | QueryResponseMessage MessageReceived(IReceivedMessage message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Core/Middleware/Metrics/MessageMetric.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Metrics; 2 | 3 | namespace MQContract.Middleware.Metrics 4 | { 5 | internal record MessageMetric(UpDownCounter Sent, UpDownCounter SentBytes, UpDownCounter Received, UpDownCounter ReceivedBytes, 6 | Histogram EncodingDuration, Histogram DecodingDuration) 7 | { 8 | public void AddEntry(MetricEntryValue entry) 9 | { 10 | if (entry.Sent) 11 | { 12 | Sent.Add(1); 13 | SentBytes.Add(entry.MessageSize); 14 | EncodingDuration.Record(entry.Duration.TotalMilliseconds); 15 | } 16 | else 17 | { 18 | Received.Add(1); 19 | ReceivedBytes.Add(entry.MessageSize); 20 | DecodingDuration.Record(entry.Duration.TotalMilliseconds); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/Utility.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.KubeMQ 2 | { 3 | internal static class Utility 4 | { 5 | internal static long ToUnixTime(DateTime timestamp) 6 | { 7 | return new DateTimeOffset(timestamp).ToUniversalTime().ToUnixTimeSeconds(); 8 | } 9 | 10 | internal static DateTime FromUnixTime(long timestamp) 11 | { 12 | try 13 | { 14 | return DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime.ToLocalTime(); 15 | } 16 | catch (Exception) 17 | { 18 | try 19 | { 20 | return DateTimeOffset.FromUnixTimeMilliseconds(timestamp/1000000).DateTime.ToLocalTime(); 21 | } 22 | catch (Exception) 23 | { 24 | return DateTime.MaxValue; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Conversion/IMessageConverter.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Conversion 2 | { 3 | /// 4 | /// Used to define a message converter. These are called upon if a 5 | /// message is received on a channel of type T but it is waiting for 6 | /// message of type V 7 | /// 8 | /// The source message type 9 | /// The destination message type 10 | public interface IMessageConverter 11 | { 12 | /// 13 | /// Called to convert a message from type T to type V 14 | /// 15 | /// The message to convert 16 | /// The source message converted to the destination type V 17 | ValueTask ConvertAsync(TSourceMessage source); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Service/IBulkPublishableMessageServiceConnection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Service 4 | { 5 | /// 6 | /// Used to implement a service that supports bulk message publishing 7 | /// 8 | public interface IBulkPublishableMessageServiceConnection : IMessageServiceConnection 9 | { 10 | /// 11 | /// Implements a publish call to publish the given messages in bulk 12 | /// 13 | /// The message to publish 14 | /// A cancellation token 15 | /// 16 | /// A transmission result instance indicating the result for each message 17 | ValueTask> BulkPublishAsync(IEnumerable messages, CancellationToken cancellationToken = new CancellationToken()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IBeforeEncodeSpecificTypeMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// This interface represents a Middleware to execute Before a specific message type is encoded 5 | /// 6 | public interface IBeforeEncodeSpecificTypeMiddleware : ISpecificTypeMiddleware 7 | { 8 | /// 9 | /// This is the method invoked as part of the Middle Ware processing during message encoding 10 | /// 11 | /// A shared context that exists from the start of this encoding instance 12 | /// The message being encoded including headers and channel 13 | /// The message, channel and header to allow for changes if desired 14 | ValueTask> BeforeMessageEncodeAsync(IContext context, EncodableMessage message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BenchMark/PublishBenchmarks/FakePublishConnection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Service; 2 | using MQContract.Messages; 3 | 4 | namespace BenchMark.PublishBenchmarks 5 | 6 | { 7 | internal class FakePublishConnection : IMessageServiceConnection 8 | { 9 | uint? IMessageServiceConnection.MaxMessageBodySize => 1024 * 1024; 10 | 11 | ValueTask IMessageServiceConnection.CloseAsync() 12 | => ValueTask.CompletedTask; 13 | 14 | ValueTask IMessageServiceConnection.PublishAsync(ServiceMessage message, CancellationToken cancellationToken) 15 | => ValueTask.FromResult(new(message.ID)); 16 | 17 | ValueTask IMessageServiceConnection.SubscribeAsync(Func messageReceived, Action errorReceived, string channel, string? group, CancellationToken cancellationToken) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IBeforeEncodeMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// This interface represents a Middleware to execute Before a message is encoded 5 | /// 6 | public interface IBeforeEncodeMiddleware : IMiddleware 7 | { 8 | /// 9 | /// This is the method invoked as part of the Middle Ware processing during message encoding 10 | /// 11 | /// The type of message being processed 12 | /// A shared context that exists from the start of this encoding instance 13 | /// The message being encoded including headers and channel 14 | /// The message, channel and header to allow for changes if desired 15 | ValueTask> BeforeMessageEncodeAsync(IContext context, EncodableMessage message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CQRS/Interfaces/IProcessorRegistrar.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Consumers; 2 | using MQContract.CQRS.Interfaces.Command; 3 | using MQContract.CQRS.Interfaces.Query; 4 | using MQContract.Messages; 5 | 6 | namespace MQContract.CQRS.Interfaces 7 | { 8 | internal interface IProcessorRegistrar 9 | { 10 | ValueTask RegisterCommandProcessorAsync(CommandConsumer processor, string? group = null, MessageFilters? messageFilters = null) 11 | where TCommand : ICommand; 12 | 13 | ValueTask RegisterCommandProcessorAsync(CommandResponseConsumer processor, string? group = null, MessageFilters? messageFilters = null) 14 | where TCommand : ICommand; 15 | ValueTask RegisterQueryProcessorAsync(QueryResponseConsumer processor, string? group = null, MessageFilters? messageFilters=null) 16 | where TQuery : IQuery; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Query/IQueryProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Query 2 | { 3 | /// 4 | /// Defines a query processor for the given type of query that expects the given response 5 | /// 6 | /// The type of query 7 | /// The type of response from the query 8 | public interface IQueryProcessor : IProcessor 9 | where TQuery : IQuery 10 | { 11 | /// 12 | /// the callback executed against the query 13 | /// 14 | /// The current invocation context for this query instance 15 | /// A cancellation token 16 | /// The result from the query execution 17 | ValueTask ProcessQueryAsync(IQueryInvocationContext invocationContext, CancellationToken cancellationToken); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Testing/Core/Consumers/BasicMessageAsyncConsumer.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Attributes; 3 | using MQContract.Interfaces; 4 | using MQContract.Interfaces.Consumers; 5 | 6 | namespace AutomatedTesting.Consumers 7 | { 8 | [Consumer(channel:"AsyncBasicMessage",group:"AsyncBasicMessageGroup")] 9 | internal class BasicMessageAsyncConsumer : IPubSubAsyncConsumer 10 | { 11 | private static readonly List> messages = []; 12 | 13 | public static List> Messages => messages; 14 | 15 | public BasicMessageAsyncConsumer() 16 | { 17 | messages.Clear(); 18 | } 19 | 20 | void IBaseConsumer.ErrorRecieved(Exception error) 21 | { } 22 | 23 | ValueTask IPubSubAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 24 | { 25 | messages.Add(message); 26 | return ValueTask.CompletedTask; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Consumers/IQueryResponseAsyncConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Consumers 4 | { 5 | /// 6 | /// Represents an Asynchronous QueryResponse Message Consumer to be registered to the ContractConnection 7 | /// 8 | /// The type of Message that is received and will be consumed 9 | /// The type of Message that is returned as a response 10 | public interface IQueryResponseAsyncConsumer : IBaseConsumer 11 | { 12 | /// 13 | /// Called when a message is received from the underlying subscript that is using this Consumer 14 | /// 15 | /// The message that was received 16 | /// The Response to the given Query Message 17 | ValueTask> MessageReceivedAsync(IReceivedMessage message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Core/Defaults/DecimalEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Defaults 2 | { 3 | internal class DecimalEncoder : ABitEncoder 4 | { 5 | private const int BitsPerDecimal = 4; 6 | 7 | protected override int ByteSize => BitsPerDecimal*sizeof(int); 8 | 9 | protected override decimal ConvertValue(ReadOnlySpan value) 10 | { 11 | var bits = new int[BitsPerDecimal]; 12 | for (var i = 0; i 6 | /// This interface represents a Middleware to execute after a Message has been encoded to a ServiceMessage from the supplied Class 7 | /// 8 | public interface IAfterEncodeMiddleware : IMiddleware 9 | { 10 | /// 11 | /// This is the method invoked as part of the Middleware processing during message encoding 12 | /// 13 | /// The class of the message type that was encoded 14 | /// A shared context that exists from the start of this encode process instance 15 | /// The resulting encoded message 16 | /// The message to allow for changes if desired 17 | ValueTask AfterMessageEncodeAsync(Type messageType, IContext context, ServiceMessage message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Resiliency.md: -------------------------------------------------------------------------------- 1 | #Resiliency 2 | 3 | To enable Resiliency in any connection you would need to call one of the instances of RegisterResiliencePolicy and supply the appropriate parameters. You can configure a policy around a given message type, a given message channel, the default and also specify a service connection name when using either the mapped or multi connection system. This allows a configuration down to a detailed level, and will prioritize the policy based on the channel first, the message type second and then the default finally. This also applies when using multiple service connections with the added level of this being attempted by the connection name first then falling back to non-connection specific. The reiliency is handled when a Transmission to an underlying service returns an Error that is not tagged as Fatal as Fatal errors cannot be retried or have their circuit broken. The fatal errors would typically be along the lines of invalid parameters or other critical library errors, not connectivity errors. The underlying system being used for this Resiliency is Polly. -------------------------------------------------------------------------------- /Abstractions/Interfaces/Service/IQueryResponseMessageServiceConnection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Service 4 | { 5 | /// 6 | /// Extends the base MessageServiceConnection Interface to Response Query messaging methodology if the underlying service supports it 7 | /// 8 | public interface IQueryResponseMessageServiceConnection : IQueryableMessageServiceConnection 9 | { 10 | /// 11 | /// Implements a call to submit a response query request into the underlying service 12 | /// 13 | /// The message to query with 14 | /// The timeout for recieving a response 15 | /// A cancellation token 16 | /// A Query Result instance based on what happened 17 | ValueTask QueryAsync(ServiceMessage message, TimeSpan timeout, CancellationToken cancellationToken = new CancellationToken()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Abstractions/Messages/MessageFilters.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Messages 2 | { 3 | /// 4 | /// Houses a set of message filtering calls for a given type. This particular record can be passed in to pubsub consumers/subscriptions 5 | /// to implement some pre-callback message filtering when receiving messages 6 | /// 7 | /// The type of message that the subscription and filter represents 8 | /// A callback filter used to filter a message by headers. This call is made prior to attempting to convert the message into the appropriate type. 9 | /// A callback filter used to filter a message by the message and or headers. This call is made after the attempt to convert the message into the approriate type. 10 | public record MessageFilters( 11 | Func>? HeaderFilter = null, 12 | Func>? MessageFilter = null 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/Interfaces/IKubeMQPingResult.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.KubeMQ.Interfaces 2 | { 3 | /// 4 | /// The definition for a PingResponse coming from KubeMQ that has a couple of extra properties available 5 | /// 6 | public interface IKubeMQPingResult 7 | { 8 | /// 9 | /// The host name for the server pinged 10 | /// 11 | string Host { get; } 12 | /// 13 | /// The current version of KubeMQ running on it 14 | /// 15 | string Version { get; } 16 | /// 17 | /// How long it took the server to respond to the request 18 | /// 19 | TimeSpan ResponseTime { get; } 20 | /// 21 | /// The Server Start Time of the host that was pinged 22 | /// 23 | DateTime ServerStartTime { get; } 24 | /// 25 | /// The Server Up Time of the host that was pinged 26 | /// 27 | TimeSpan ServerUpTime { get; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Connectors/InMemory/Subscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Service; 2 | using System.Threading.Channels; 3 | 4 | namespace MQContract.InMemory 5 | { 6 | internal class Subscription(MessageGroup group, Func messageRecieved) : IServiceSubscription 7 | { 8 | private readonly (Guid id, Channel channel) registration = group.Register(); 9 | 10 | public void Start() 11 | { 12 | Task.Run(async () => 13 | { 14 | while (await registration.channel.Reader.WaitToReadAsync()) 15 | { 16 | var message = await registration.channel.Reader.ReadAsync(); 17 | await messageRecieved(message).ConfigureAwait(false); 18 | } 19 | }); 20 | } 21 | 22 | ValueTask IServiceSubscription.EndAsync() 23 | { 24 | registration.channel.Writer.TryComplete(); 25 | group.Unregister(registration.id); 26 | return ValueTask.CompletedTask; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Testing/Core/Encoders/TestMessageEncoderWithInjection.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using AutomatedTesting.ServiceInjection; 3 | using MQContract.Interfaces.Encoding; 4 | using System.Text; 5 | 6 | namespace AutomatedTesting.Encoders 7 | { 8 | internal class TestMessageEncoderWithInjection(IInjectableService injectableService) 9 | : IMessageTypeEncoder 10 | { 11 | public ValueTask DecodeAsync(Stream stream) 12 | { 13 | var message = Encoding.ASCII.GetString(new BinaryReader(stream).ReadBytes((int)stream.Length)); 14 | Assert.StartsWith($"{injectableService.Name}:", message); 15 | return ValueTask.FromResult(new CustomEncoderWithInjectionMessage(message.Substring($"{injectableService.Name}:".Length))); 16 | } 17 | 18 | public ValueTask EncodeAsync(CustomEncoderWithInjectionMessage message) 19 | => ValueTask.FromResult(Encoding.ASCII.GetBytes($"{injectableService.Name}:{message.TestName}")); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BenchMark/Encoders/EncodedAnnouncementEncoder.cs: -------------------------------------------------------------------------------- 1 | using BenchMark.Messages; 2 | using MQContract.Interfaces.Encoding; 3 | using System.Text.Json; 4 | 5 | namespace BenchMark.Encoders 6 | { 7 | internal class EncodedAnnouncementEncoder : IMessageTypeEncoder 8 | { 9 | private static JsonSerializerOptions JsonOptions => new() 10 | { 11 | WriteIndented=false, 12 | AllowTrailingCommas=true, 13 | PropertyNameCaseInsensitive=true, 14 | ReadCommentHandling=JsonCommentHandling.Skip, 15 | TypeInfoResolver = MyJsonContext.Default 16 | }; 17 | 18 | async ValueTask IMessageTypeEncoder.DecodeAsync(Stream stream) 19 | => await JsonSerializer.DeserializeAsync(stream,options:JsonOptions); 20 | 21 | ValueTask IMessageTypeEncoder.EncodeAsync(EncodedAnnouncement message) 22 | => ValueTask.FromResult(JsonSerializer.SerializeToUtf8Bytes(message, options: JsonOptions)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CQRS/Consumers/QueryResponseConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Contexts; 2 | using MQContract.CQRS.Interfaces.Query; 3 | using MQContract.Interfaces; 4 | using MQContract.Interfaces.Consumers; 5 | using MQContract.Messages; 6 | 7 | namespace MQContract.CQRS.Consumers 8 | { 9 | internal class QueryResponseConsumer(IQueryProcessor queryProcessor, CqrsConnection connection) 10 | : IQueryResponseAsyncConsumer 11 | where TQuery : IQuery 12 | { 13 | void IBaseConsumer.ErrorRecieved(Exception error) 14 | => queryProcessor.ErrorRecieved(error); 15 | 16 | async ValueTask> IQueryResponseAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 17 | { 18 | await using var context = new QueryInvocationContext(message, connection); 19 | var result = await queryProcessor.ProcessQueryAsync(context, context.CancellationTokenSource.Token); 20 | return new(result, context.Headers); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Roger Castaldo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Core/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract 2 | { 3 | internal static class Constants 4 | { 5 | private const string BaseActivityName = "MQContract"; 6 | public const string PublishActivityName = $"{BaseActivityName}.PublishMessage"; 7 | public const string PublishQueryActivityName = $"{BaseActivityName}.PublishQueryMessage"; 8 | public const string BulkPublishActivityName = $"{BaseActivityName}.BulkPublishMessages"; 9 | public const string ConsumeActivityName = $"{BaseActivityName}.ConsumeMessage"; 10 | public const string ConsumeQueryActivityName = $"{BaseActivityName}.ConsumeQueryMessage"; 11 | public const string ProduceQueryResponseActivityName = $"{BaseActivityName}.ProduceQueryResponse"; 12 | public const string ConsumeQueryResponseActivityName = $"{BaseActivityName}.ConsumeQueryResponse"; 13 | public const string PublishBulkMessagesMessageEvent = "BulkMessagePublished"; 14 | public const string MessageFilteredName = $"{BaseActivityName}.MessageFiltered"; 15 | 16 | public const string BulkPublishCountTag = "mqcontract.bulkmessagecount"; 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CQRS/Extensions/IContractConnectionExtension.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Interfaces; 2 | using MQContract.Interfaces; 3 | 4 | namespace MQContract.CQRS.Extensions 5 | { 6 | /// 7 | /// Houses extension methods for the CQRS connections linked to a Contract Connection 8 | /// 9 | public static class IContractConnectionExtension 10 | { 11 | /// 12 | /// Creates a CQRS connection instance linked to the given contract connection 13 | /// WARNING: THe Contract Connection cannot be a MultiService style connection, it only supports the single instance or mapped. 14 | /// 15 | /// The contract connection it will be linked to. 16 | /// The channel to use for distributing cancelling token Cancel calls 17 | /// 18 | public static ICQRSConnection CreateCQRSConnection(this IContractConnection contractConnection, string? cancelationTokenChannel = null) 19 | => new CqrsConnection(contractConnection, cancelationTokenChannel); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Testing/Core/Encryptors/TestMessageEncryptor.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Interfaces.Encrypting; 3 | 4 | namespace AutomatedTesting.Encryptors 5 | { 6 | internal class TestMessageEncryptor : IMessageTypeEncryptor 7 | { 8 | private const string HeaderKey = "TestMessageEncryptorKey"; 9 | private const string HeaderValue = "TestMessageEncryptorValue"; 10 | 11 | public ValueTask DecryptAsync(Stream stream, MessageHeader headers) 12 | { 13 | Assert.IsNotNull(headers); 14 | Assert.IsTrue(headers.Keys.Contains(HeaderKey)); 15 | Assert.AreEqual(HeaderValue, headers[HeaderKey]); 16 | var data = new BinaryReader(stream).ReadBytes((int)stream.Length); 17 | return ValueTask.FromResult(new MemoryStream(data.Reverse().ToArray())); 18 | } 19 | 20 | public ValueTask EncryptAsync(byte[] data) 21 | => ValueTask.FromResult(new( 22 | new([new(HeaderKey, HeaderValue)]), 23 | [.. data.Reverse()] 24 | )); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CQRS/Consumers/CommandResponseConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Contexts; 2 | using MQContract.CQRS.Interfaces.Command; 3 | using MQContract.Interfaces; 4 | using MQContract.Interfaces.Consumers; 5 | using MQContract.Messages; 6 | 7 | namespace MQContract.CQRS.Consumers 8 | { 9 | internal class CommandResponseConsumer(ICommandProcessor commandProcessor, CqrsConnection connection) 10 | : IQueryResponseAsyncConsumer 11 | where TCommand : ICommand 12 | { 13 | void IBaseConsumer.ErrorRecieved(Exception error) 14 | => commandProcessor.ErrorRecieved(error); 15 | 16 | async ValueTask> IQueryResponseAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 17 | { 18 | await using var context = new CommandInvocationContext(message, connection); 19 | var result = await commandProcessor.ProcessCommandAsync(context, context.CancellationTokenSource.Token); 20 | return new(result, context.Headers); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CQRS/Attributes/QueryAttribute.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Attributes; 2 | 3 | namespace MQContract.CQRS.Attributes 4 | { 5 | /// 6 | /// Use this attribute to specify the Channel, TypeName, TypeVersion, Response Channel and or Response timeout 7 | /// for the Query being defined 8 | /// 9 | /// The channel to be used 10 | /// The query type to use 11 | /// The query type version to use 12 | /// The responce channel to be used when an underlying service connection does not support QueryResponse or Inbox 13 | /// The query response timeout to default to 14 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 15 | public class QueryAttribute(string channel, string? typeName = null, string? typeVersion = null, 16 | string? responseChannel = null, int responseTimeoutMilliseconds = 60*1000) : 17 | QueryMessageAttribute(channel,typeName,typeVersion,responseChannel,responseTimeoutMilliseconds) 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IBeforeDecodeMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// This interface represents a Middleware to execute before decoding a ServiceMessage 5 | /// 6 | public interface IBeforeDecodeMiddleware : IMiddleware 7 | { 8 | /// 9 | /// This is the method invoked as part of the Middleware processing prior to the message decoding 10 | /// 11 | /// A shared context that exists from the start of this decode process instance 12 | /// The id of the message 13 | /// The message type id 14 | /// The channel the message was recieved on 15 | /// The decodable message housing headers and the data 16 | /// The message header and data to allow for changes if desired 17 | ValueTask BeforeMessageDecodeAsync(IContext context, string id, string messageTypeID, string messageChannel, DecodableMessage message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Core/Factories/AConverter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using MQContract.Attributes; 3 | using MQContract.Interfaces.Conversion; 4 | using MQContract.Interfaces.Messages; 5 | using System.Reflection; 6 | 7 | namespace MQContract.Factories 8 | { 9 | internal abstract class AConverter 10 | : IConversionPath 11 | { 12 | private readonly string messageTypeName = $"{Utility.GetCustomAttribute()?.TypeName??Utility.TypeName()}-{typeof(TSourceMessageType).GetCustomAttribute()?.TypeVersion.ToString()??"0.0.0.0"}"; 13 | 14 | protected abstract ValueTask ConvertMessageAsync(ILogger? logger, IEncodedMessage message, Stream? dataStream); 15 | 16 | ValueTask IConversionPath.ConvertMessageAsync(ILogger? logger, IEncodedMessage message, Stream? dataStream) 17 | => ConvertMessageAsync(logger, message, dataStream); 18 | 19 | bool IConversionPath.IsMatch(string metaData) 20 | => messageTypeName.Equals(metaData, StringComparison.InvariantCulture); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Core/Middleware/ChannelMappingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Middleware; 2 | 3 | namespace MQContract.Middleware 4 | { 5 | internal class ChannelMappingMiddleware(ChannelMapper? channelMapper) 6 | : IBeforeEncodeMiddleware 7 | { 8 | private async ValueTask MapChannel(Context context, string? channel) 9 | { 10 | if (channelMapper==null || channel==null) 11 | return channel; 12 | return await channelMapper.MapChannel(context.MapDirection, channel); 13 | } 14 | 15 | async ValueTask> IBeforeEncodeMiddleware.BeforeMessageEncodeAsync(IContext context, EncodableMessage message) 16 | { 17 | var mappedChannel = await MapChannel((Context)context, message.Channel); 18 | context.Activity?.AddEvent(new("MessageChannelMapped", tags: new([ 19 | new(OpenTelemetryMiddleware.InitialChannelKey,message.Channel), 20 | new($"{OpenTelemetryMiddleware.KeyBase}.mappedchannel",mappedChannel) 21 | ]))); 22 | return new(message.MessageHeader, message.Message, mappedChannel); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/unit-test-report.yml: -------------------------------------------------------------------------------- 1 | name: "Dot Net Test Reporter" 2 | 3 | on: 4 | pull_request_target: 5 | types: [ opened, synchronize ] 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: | 22 | 8.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore -p:TargetFramework=net8.0 -p:BlockMD=true Testing/Core 25 | - name: Build-8.0 26 | run: dotnet build --framework net8.0 -p:BlockMD=true --no-restore Testing/Core 27 | - name: Test-8.0 28 | run: dotnet test --framework net8.0 -p:BlockMD=true --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"Code Coverage" Testing/Core 29 | - name: report results 30 | uses: bibipkins/dotnet-test-reporter@v1.4.0 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | comment-title: 'Unit Test Results' 34 | results-path: ./**/*.trx 35 | coverage-path: ./**/*.coverage 36 | coverage-threshold: 90 37 | -------------------------------------------------------------------------------- /.github/workflows/unittests8x.yml: -------------------------------------------------------------------------------- 1 | name: .NET-Test-8x 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | pull_request: 7 | branches: [ 'master' ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: | 20 | 8.0.x 21 | - name: Restore dependencies testing core 22 | run: dotnet restore -p:TargetFramework=net8.0 -p:BlockMD=true Testing/Core 23 | - name: Restore dependencies testing cqrs 24 | run: dotnet restore -p:TargetFramework=net8.0 -p:BlockMD=true Testing/CQRSTesting 25 | - name: Build-8.0 tesitng core 26 | run: dotnet build --framework net8.0 -p:BlockMD=true --no-restore Testing/Core 27 | - name: Build-8.0 tesitng cqrs 28 | run: dotnet build --framework net8.0 -p:BlockMD=true --no-restore Testing/CQRSTesting 29 | - name: Test-8.0 core 30 | run: dotnet test --framework net8.0 -p:BlockMD=true --no-build --verbosity normal Testing/Core 31 | - name: Test-8.0 cqrs 32 | run: dotnet test --framework net8.0 -p:BlockMD=true --no-build --verbosity normal Testing/CQRSTesting 33 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Encoding/IMessageTypeEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Encoding 2 | { 3 | /// 4 | /// Used to define a specific encoder for the message type of T 5 | /// This is used to override the default Json and the Global one for the connection if specified 6 | /// 7 | /// The type of message that this encoder supports 8 | public interface IMessageTypeEncoder 9 | { 10 | /// 11 | /// Called to encode the message into a byte array 12 | /// 13 | /// The message value to encode 14 | /// The message encoded as a byte array 15 | ValueTask EncodeAsync(TMessage message); 16 | /// 17 | /// Called to decode the message from a byte stream into the specified type 18 | /// 19 | /// The byte stream containing the encoded message 20 | /// null if the Decode fails, otherwise an instance of the message decoded from the stream 21 | ValueTask DecodeAsync(Stream stream); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Testing/Core/ConnectionTests/SingleService/PingTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using MQContract; 3 | using MQContract.Interfaces.Service; 4 | 5 | namespace AutomatedTesting.ConnectionTests.SingleService 6 | { 7 | [TestClass] 8 | public class PingTests 9 | { 10 | [TestMethod] 11 | public async Task TestPingAsync() 12 | { 13 | #region Arrange 14 | var pingResult = new PingResult("TestHost", "1.0.0", TimeSpan.FromSeconds(5)); 15 | 16 | var serviceConnection = new Mock(); 17 | serviceConnection.Setup(x => x.PingAsync()) 18 | .ReturnsAsync(pingResult); 19 | 20 | var contractConnection = ContractConnection.Instance(serviceConnection.Object); 21 | #endregion 22 | 23 | #region Act 24 | var result = await contractConnection.PingAsync(); 25 | #endregion 26 | 27 | #region Assert 28 | Assert.IsNotNull(result); 29 | Assert.AreEqual(pingResult, result); 30 | #endregion 31 | 32 | #region Verify 33 | serviceConnection.Verify(x => x.PingAsync(), Times.Once); 34 | #endregion 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Abstractions/Attributes/MessageAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Attributes 2 | { 3 | /// 4 | /// Use this attribute to specify the Channel, TypeName and or Type Version of the 5 | /// Message being defined 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// The channel to be used 11 | /// The message type to use 12 | /// The message version to use 13 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 14 | public class MessageAttribute(string? channel = null, string? typeName = null, string? typeVersion = null) : Attribute 15 | { 16 | /// 17 | /// The Channel specified 18 | /// 19 | public string? Channel => channel; 20 | 21 | /// 22 | /// The name of the message type used when transmitting 23 | /// 24 | public string? TypeName => typeName; 25 | 26 | /// 27 | /// The version number to tag this message with during transmission 28 | /// 29 | public Version TypeVersion => new(typeVersion??"0.0.0.0"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Abstractions/Attributes/ConsumerAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Attributes 2 | { 3 | /// 4 | /// Use this attribute to define the Channel, Group and/or IngoreMessageTypeHeader flag 5 | /// for a given Consumer 6 | /// 7 | /// The channel the consumer will listen on 8 | /// The group the consumer will register as 9 | /// A falg to indicate if ignoring the message type is desired 10 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 11 | public class ConsumerAttribute(string? channel=null,string? group=null,bool ignoreMessageTypeHeader=false) : Attribute 12 | { 13 | /// 14 | /// The channel to register the consumer on 15 | /// 16 | public string? Channel => channel; 17 | 18 | /// 19 | /// The group to register the consumer to 20 | /// 21 | public string? Group => group; 22 | 23 | /// 24 | /// Indicates if the message type should be ignored 25 | /// 26 | public bool IgnoreMessageTypeHeader => ignoreMessageTypeHeader; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Samples/GooglePubSubSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Google.Api.Gax; 2 | using Google.Cloud.PubSub.V1; 3 | using Grpc.Core; 4 | using Messages; 5 | using MQContract.GooglePubSub; 6 | 7 | const string projectId = "sample-project-id"; 8 | const string endpoint = "localhost:8085"; 9 | 10 | var publisherServiceBuilder = new PublisherServiceApiClientBuilder 11 | { 12 | EmulatorDetection = EmulatorDetection.EmulatorOrProduction, 13 | Endpoint=endpoint, 14 | ChannelCredentials = ChannelCredentials.Insecure 15 | }; 16 | 17 | var subscriberServiceBuilder = new SubscriberServiceApiClientBuilder 18 | { 19 | EmulatorDetection = EmulatorDetection.EmulatorOrProduction, 20 | Endpoint=endpoint, 21 | ChannelCredentials = ChannelCredentials.Insecure 22 | }; 23 | 24 | var serviceConnection = new Connection(projectId, publisherServiceBuilder, subscriberServiceBuilder); 25 | 26 | foreach (var name in new string[] { "Arrivals", "Greeting", "Greeting.Response", "StoredArrivals" }) 27 | { 28 | var topicName = new TopicName(projectId, name); 29 | if (await serviceConnection.PublisherServiceApi.GetTopicAsync(topicName)==null) 30 | await serviceConnection.PublisherServiceApi.CreateTopicAsync(topicName); 31 | } 32 | 33 | 34 | 35 | 36 | await SampleExecution.ExecuteSample(serviceConnection, "GooglePubSub"); -------------------------------------------------------------------------------- /Connectors/Redis/Subscriptions/QueryResponseSubscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using StackExchange.Redis; 3 | 4 | namespace MQContract.Redis.Subscriptions 5 | { 6 | internal class QueryResponseSubscription(Func> messageReceived, Action errorReceived, IDatabase database, Guid connectionID, string channel, string? group) 7 | : SubscriptionBase(errorReceived, database, connectionID, channel, group) 8 | { 9 | protected override async ValueTask ProcessMessage(StreamEntry streamEntry, string channel, string? group) 10 | { 11 | (var message, var responseChannel, var timeout) = Connection.ConvertMessage( 12 | streamEntry.Values, 13 | channel, 14 | () => Acknowledge(streamEntry.Id) 15 | ); 16 | var result = await messageReceived(message); 17 | if (result!=null) 18 | { 19 | await Database.StreamDeleteAsync(Channel, [streamEntry.Id]); 20 | await Database.StringSetAsync(responseChannel, Connection.EncodeMessage(result), expiry: (timeout == null ? Expiration.Default : new Expiration(DateTime.UtcNow.Add(timeout.Value)))); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Encrypting/IMessageEncryptor.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Encrypting 4 | { 5 | /// 6 | /// An implementation of this is used to encrypt/decrypt message bodies when 7 | /// specified for a connection. This is to allow for extended message security 8 | /// if desired. 9 | /// 10 | public interface IMessageEncryptor 11 | { 12 | /// 13 | /// Called to decrypt the message body stream received as a message 14 | /// 15 | /// The stream representing the message body binary data 16 | /// The message headers that were provided by the message 17 | /// A decrypted stream of the message body 18 | ValueTask DecryptAsync(Stream stream, MessageHeader headers); 19 | 20 | /// 21 | /// Called to encrypt the message body prior to transmitting a message 22 | /// 23 | /// The original unencrypted body data 24 | /// An encrypted byte array of the message body and any headers that might be needed 25 | ValueTask EncryptAsync(byte[] data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Testing/Core/Encryptors/TestMessageEncryptorWithInjection.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using AutomatedTesting.ServiceInjection; 3 | using MQContract.Interfaces.Encrypting; 4 | 5 | namespace AutomatedTesting.Encryptors 6 | { 7 | internal class TestMessageEncryptorWithInjection(IInjectableService injectableService) 8 | : IMessageTypeEncryptor 9 | { 10 | private const string HeaderKey = "TestMessageEncryptorWithInjectionKey"; 11 | 12 | public ValueTask DecryptAsync(Stream stream, MessageHeader headers) 13 | { 14 | Assert.IsNotNull(headers); 15 | Assert.IsTrue(headers.Keys.Contains(HeaderKey)); 16 | Assert.AreEqual(injectableService.Name, headers[HeaderKey]); 17 | var data = new BinaryReader(stream).ReadBytes((int)stream.Length); 18 | return ValueTask.FromResult(new MemoryStream(data.Reverse().ToArray())); 19 | } 20 | 21 | public ValueTask EncryptAsync(byte[] data) 22 | => ValueTask.FromResult(new ( 23 | new Dictionary([ 24 | new(HeaderKey,injectableService.Name) 25 | ]), 26 | [.. data.Reverse()] 27 | )); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Samples/NATSSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Messages; 2 | using MQContract; 3 | using MQContract.NATS; 4 | using NATS.Client.JetStream.Models; 5 | using OpenTelemetry; 6 | using OpenTelemetry.Logs; 7 | using OpenTelemetry.Metrics; 8 | using OpenTelemetry.Resources; 9 | using OpenTelemetry.Trace; 10 | 11 | 12 | var serviceName = "MQContract"; 13 | 14 | using var tracerProvider = Sdk.CreateTracerProviderBuilder() 15 | .AddSource(serviceName) // Tracks activities from this source 16 | .SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder.CreateDefault().AddService(serviceName)) 17 | .AddOtlpExporter(options => 18 | { 19 | options.Endpoint = new("http://localhost:4317"); 20 | }) // Optional: Export to OTLP endpoint 21 | .Build(); 22 | 23 | 24 | var serviceConnection = new Connection(new NATS.Client.Core.NatsOpts() 25 | { 26 | LoggerFactory=new Microsoft.Extensions.Logging.LoggerFactory(), 27 | Name="NATSSample" 28 | }); 29 | 30 | var streamConfig = new StreamConfig("StoredArrivalsStream", ["StoredArrivals"]) 31 | { 32 | MaxBytes = 100 * 1024 * 1024 33 | }; 34 | await serviceConnection.CreateStreamAsync(streamConfig); 35 | 36 | var mapper = new ChannelMapper() 37 | .AddPublishSubscriptionMap("StoredArrivals", "StoredArrivalsStream"); 38 | 39 | await SampleExecution.ExecuteSample(serviceConnection, "NatsIO", mapper); -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IAfterDecodeSpecificTypeMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// 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 5 | /// 6 | public interface IAfterDecodeSpecificTypeMiddleware : ISpecificTypeMiddleware 7 | { 8 | /// 9 | /// This is the method invoked as part of the Middleware processing during message decoding 10 | /// 11 | /// A shared context that exists from the start of this decode process instance 12 | /// The id of the message 13 | /// The decoded message including the headers 14 | /// The timestamp of when the message was recieved 15 | /// The timestamp of when the message was decoded into a Class 16 | /// The message and header to allow for changes if desired 17 | ValueTask> AfterMessageDecodeAsync(IContext context, string ID, DecodedMessage message, DateTime receivedTimestamp, DateTime processedTimeStamp); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Abstractions/Messages/MultiTransmissionResult.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Messages 2 | { 3 | /// 4 | /// Houses the result of a transmission into an underlying service with the corresponding name 5 | /// 6 | /// The unique name of the underlying service that was used to transmit 7 | /// An error message if an error occured 8 | public record ChildTransmissionResult(string ServiceName, ErrorMessage? Error = null) 9 | { 10 | /// 11 | /// Flag to indicate if the result is an error 12 | /// 13 | public bool IsError => !string.IsNullOrWhiteSpace(Error?.Message); 14 | } 15 | 16 | /// 17 | /// Houses the result of a transmission into the system when using the MultiService method 18 | /// 19 | /// The unique ID of the message that was transmitted 20 | /// Houses all the results from each underlying system connection used 21 | public record MultiTransmissionResult(string ID, IEnumerable Results) 22 | { 23 | /// 24 | /// Flag to indicate if there are any errors in the result 25 | /// 26 | public bool HasError => Results.Any(ctr => ctr.IsError); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Core/Subscriptions/SubscriptionCollection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces; 2 | 3 | namespace MQContract.Subscriptions 4 | { 5 | #pragma warning disable S3881 // "IDisposable" should be implemented correctly 6 | internal class SubscriptionCollection(IEnumerable subscriptions) 7 | #pragma warning restore S3881 // "IDisposable" should be implemented correctly 8 | : ISubscription 9 | { 10 | private bool disposedValue; 11 | 12 | ValueTask IAsyncDisposable.DisposeAsync() 13 | => subscriptions.Select(s => s.DisposeAsync()).WhenAll(); 14 | 15 | ValueTask ISubscription.EndAsync() 16 | => subscriptions.Select(s => s.EndAsync()).WhenAll(); 17 | 18 | protected virtual void Dispose(bool disposing) 19 | { 20 | if (!disposedValue) 21 | { 22 | if (disposing) 23 | { 24 | foreach (var subscription in subscriptions) 25 | subscription.Dispose(); 26 | } 27 | disposedValue=true; 28 | } 29 | } 30 | 31 | void IDisposable.Dispose() 32 | { 33 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 34 | Dispose(disposing: true); 35 | GC.SuppressFinalize(this); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Encoding/IMessageEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Encoding 2 | { 3 | /// 4 | /// An implementation of this is used to encode/decode message bodies when 5 | /// specified for a connection. This is to allow for an override of the 6 | /// default encoding of Json for the messages. 7 | /// 8 | public interface IMessageEncoder 9 | { 10 | /// 11 | /// Called to encode a message into a byte array 12 | /// 13 | /// The type of message being encoded 14 | /// The message being encoded 15 | /// A byte array of the message in it's encoded form that will be transmitted 16 | ValueTask EncodeAsync(TMessage message); 17 | 18 | /// 19 | /// Called to decode a message from a byte array 20 | /// 21 | /// The type of message being decoded 22 | /// A stream representing the byte array data that was transmitted as the message body in KubeMQ 23 | /// Null when fails or the value of T that was encoded inside the stream 24 | ValueTask DecodeAsync(Stream stream); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/IAfterDecodeMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces.Middleware 2 | { 3 | /// 4 | /// This interface represents a Middleware to execute after a Message has been decoded from a ServiceMessage to the expected Class 5 | /// 6 | public interface IAfterDecodeMiddleware : IMiddleware 7 | { 8 | /// 9 | /// This is the method invoked as part of the Middleware processing during message decoding 10 | /// 11 | /// This will be the type of the Message that was decoded 12 | /// A shared context that exists from the start of this decode process instance 13 | /// The id of the message 14 | /// The Decoded service message that includes both the message and headers 15 | /// The timestamp of when the message was recieved 16 | /// The timestamp of when the message was decoded into a Class 17 | /// The message and header to allow for changes if desired 18 | ValueTask> AfterMessageDecodeAsync(IContext context, string ID, DecodedMessage message, DateTime receivedTimestamp, DateTime processedTimeStamp); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abstractions/Messages/RecievedInboxServiceMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace MQContract.Messages 4 | { 5 | /// 6 | /// A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection 7 | /// 8 | /// The unique ID of the message 9 | /// The message type id which is used for decoding to a class 10 | /// The channel the message was received on 11 | /// The message headers that came through 12 | /// The query message correlation id supplied by the query call to tie to the response 13 | /// The binary content of the message that should be the encoded class 14 | /// The acknowledgement callback to be called when the message is received if the underlying service requires it 15 | [ExcludeFromCodeCoverage(Justification = "This is a record class and has nothing to test")] 16 | public record ReceivedInboxServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, Guid CorrelationID, ReadOnlyMemory Data, Func? Acknowledge = null) 17 | : ReceivedServiceMessage(ID, MessageTypeID, Channel, Header, Data, Acknowledge) 18 | { } 19 | } 20 | -------------------------------------------------------------------------------- /Clean-BuildFolders.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Deletes all 'bin' and 'obj' folders recursively under a specified path. 4 | 5 | .PARAMETER Path 6 | The root directory to search for 'bin' and 'obj' folders. 7 | 8 | .EXAMPLE 9 | .\Clean-BuildFolders.ps1 -Path "C:\Repos" 10 | #> 11 | 12 | param( 13 | [Parameter(Mandatory = $true)] 14 | [string]$Path 15 | ) 16 | 17 | # Verify the path exists 18 | if (-not (Test-Path $Path)) { 19 | Write-Host "The path '$Path' does not exist." -ForegroundColor Red 20 | exit 21 | } 22 | 23 | Write-Host "Scanning for 'bin' and 'obj' folders under: $Path" -ForegroundColor Cyan 24 | 25 | # Find all directories named 'bin' or 'obj' recursively 26 | $folders = Get-ChildItem -Path $Path -Directory -Recurse -ErrorAction SilentlyContinue | 27 | Where-Object { $_.Name -in @('bin', 'obj') } 28 | 29 | if ($folders.Count -eq 0) { 30 | Write-Host "No 'bin' or 'obj' folders found." -ForegroundColor Yellow 31 | exit 32 | } 33 | 34 | foreach ($folder in $folders) { 35 | try { 36 | Write-Host "Deleting: $($folder.FullName)" -ForegroundColor Yellow 37 | Remove-Item -Path $folder.FullName -Recurse -Force -ErrorAction Stop 38 | Write-Host "Deleted: $($folder.FullName)" -ForegroundColor Green 39 | } catch { 40 | Write-Host "Failed to delete $($folder.FullName): $($_.Exception.Message)" -ForegroundColor Red 41 | } 42 | } 43 | 44 | Write-Host "Cleanup complete." -ForegroundColor Cyan -------------------------------------------------------------------------------- /Core/Middleware/Context.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Middleware; 2 | using System.Diagnostics; 3 | 4 | namespace MQContract.Middleware 5 | { 6 | internal class Context : IContext 7 | { 8 | private const string MapTypeKey = "_MapType"; 9 | private const string MaxMessageSizeKey = "_MaxMessageSize"; 10 | private readonly Dictionary values = []; 11 | 12 | public Context(ChannelMapper.MapTypes mapDirection, Activity? activity, uint? maxMessageSize = null, Type? expectedType = null) 13 | { 14 | this[MapTypeKey] = mapDirection; 15 | Activity = activity; 16 | this[MaxMessageSizeKey] = maxMessageSize??int.MaxValue; 17 | this[EncryptionMiddleware.ExpectedTypeKey] = expectedType; 18 | } 19 | 20 | public object? this[string key] 21 | { 22 | get => values.TryGetValue(key, out var value) ? value : null; 23 | set 24 | { 25 | if (value==null) 26 | values.Remove(key); 27 | else 28 | values.TryAdd(key, value); 29 | } 30 | } 31 | 32 | public Activity? Activity { get; private init; } 33 | 34 | public uint MaxMessageSize 35 | => (uint)this[MaxMessageSizeKey]!; 36 | 37 | public ChannelMapper.MapTypes MapDirection 38 | => (ChannelMapper.MapTypes)this[MapTypeKey]!; 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Abstractions/Messages/RecievedServiceMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace MQContract.Messages 4 | { 5 | /// 6 | /// A Received Service Message that gets passed back up into the Contract Connection when a message is received from the underlying service connection 7 | /// 8 | /// The unique ID of the message 9 | /// The message type id which is used for decoding to a class 10 | /// The channel the message was received on 11 | /// The message headers that came through 12 | /// The binary content of the message that should be the encoded class 13 | /// The acknowledgement callback to be called when the message is received if the underlying service requires it 14 | [ExcludeFromCodeCoverage(Justification = "This is a record class and has nothing to test")] 15 | public record ReceivedServiceMessage(string ID, string MessageTypeID, string Channel, MessageHeader Header, ReadOnlyMemory Data, Func? Acknowledge = null) 16 | : ServiceMessage(ID, MessageTypeID, Channel, Header, Data) 17 | { 18 | /// 19 | /// A timestamp for when the message was received 20 | /// 21 | public DateTime ReceivedTimestamp { get; private init; } = DateTime.Now; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Samples/KafkaSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Confluent.SchemaRegistry; 2 | using Messages; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using MQContract.Kafka; 5 | using MQContract.Kafka.Middleware; 6 | 7 | var cacheOptions = new MemoryCacheOptions(); 8 | 9 | using var cache = new MemoryCache(cacheOptions); 10 | 11 | 12 | var serviceConnection = new Connection(new Confluent.Kafka.ClientConfig() 13 | { 14 | ClientId="KafkaSample", 15 | BootstrapServers="localhost:9092" 16 | }); 17 | 18 | #pragma warning disable S1075 // URIs should not be hardcoded 19 | //This is a sample program with a localhost connection so this is necessary 20 | var schemaRegistryConfig = new SchemaRegistryConfig 21 | { 22 | // URL to your Schema Registry (local, dev, or Confluent Cloud) 23 | Url = "http://localhost:8081", 24 | 25 | // Optional authentication if using Confluent Cloud 26 | // BasicAuthCredentialsSource = AuthCredentialsSource.UserInfo, 27 | // BasicAuthUserInfo = "API_KEY:API_SECRET" 28 | }; 29 | #pragma warning restore S1075 // URIs should not be hardcoded 30 | 31 | using var schemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); 32 | 33 | await SampleExecution.ExecuteSample(serviceConnection, "Kafka", middlewares: [ 34 | new SchemaValidationMiddleware( 35 | schemaRegistryClient, 36 | mapMessageSchemaName:(messageType,messageChannel,messageTypeId)=>ValueTask.FromResult($"{messageChannel}-{messageTypeId}"), 37 | cache: cache 38 | ) 39 | ]); -------------------------------------------------------------------------------- /Testing/Core/Constants.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.Messages; 2 | using MQContract.Attributes; 3 | using System.Reflection; 4 | 5 | namespace AutomatedTesting 6 | { 7 | internal static class Constants 8 | { 9 | public const string BasicMessageType = "BasicMessage-0.0.0.0"; 10 | public const string CustomEncoderMessageType = "CustomEncoderMessage-0.0.0.0"; 11 | public const string CustomEncryptorMessageType = "CustomEncryptorMessage-0.0.0.0"; 12 | public const string CustomEncoderWithInjectionMessageType = "CustomEncoderWithInjectionMessage-0.0.0.0"; 13 | public const string CustomEncryptorWithInjectionMessageType = "CustomEncryptorWithInjectionMessage-0.0.0.0"; 14 | public const string NoChannelMessageType = "NoChannelMessage-0.0.0.0"; 15 | public const string BasicQueryMessageType = "BasicQueryMessage-0.0.0.0"; 16 | public const string TimeoutMessageType = "TimeoutMessage-0.0.0.0"; 17 | public readonly static string NamedAndVersionedMessageType = $"{typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.TypeName}-{typeof(NamedAndVersionedMessage).GetCustomAttribute(false)?.TypeVersion}"; 18 | public const string HealthyDescription = "MQContract service connection available"; 19 | public const string UnHealthyDescription = "MQContract service connection unavailable"; 20 | public const string DegradedDescription = "1 or more service connection(s) are unavailable"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Middleware/Records.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Middleware 4 | { 5 | /// 6 | /// Represents a decodable message that will run through the middleware 7 | /// 8 | /// The headers supplied with the message 9 | /// The message data 10 | public readonly record struct DecodableMessage(MessageHeader MessageHeader,ReadOnlyMemory Data); 11 | 12 | /// 13 | /// Represents a decoded message that will run through the middleware 14 | /// 15 | /// The type of message it is 16 | /// The headers supplied with the message 17 | /// The decoded message 18 | public readonly record struct DecodedMessage(MessageHeader MessageHeader, TMessage Message); 19 | 20 | /// 21 | /// Represents an encodable message that will run through the middleware 22 | /// 23 | /// The type of message it is 24 | /// THe headers supplied with the message 25 | /// The message itself 26 | /// The channel the message was request to go through 27 | public readonly record struct EncodableMessage(MessageHeader MessageHeader, TMessage Message, string? Channel); 28 | } 29 | -------------------------------------------------------------------------------- /CQRS/Consumers/CancellationRequestConsumer.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces; 2 | using MQContract.Interfaces.Consumers; 3 | using System.Collections.Concurrent; 4 | 5 | namespace MQContract.CQRS.Consumers 6 | { 7 | internal class CancellationRequestConsumer(ConcurrentDictionary<(Guid messageId, Guid correlationId, Guid? causationId), CancellationTokenSource> invocationInstances) : IPubSubAsyncConsumer 8 | { 9 | void IBaseConsumer.ErrorRecieved(Exception error) 10 | {} 11 | 12 | async ValueTask IPubSubAsyncConsumer.MessageReceivedAsync(IReceivedMessage message) 13 | { 14 | var keys = invocationInstances.Keys.Where(key => Equals(message.Message.CorrelationId, key.correlationId) 15 | && (Equals(message.Message.MessageId,key.messageId) || Equals(message.Message.MessageId,key.causationId))).ToArray(); 16 | foreach(var key in keys) 17 | { 18 | if (invocationInstances.TryRemove(key,out var cancellationTokenSource)) 19 | { 20 | try 21 | { 22 | if (!cancellationTokenSource.IsCancellationRequested) 23 | await cancellationTokenSource.CancelAsync(); 24 | } 25 | catch 26 | { 27 | //no exception catch needed 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.KubeMQ 2 | { 3 | /// 4 | /// Thrown when an error occurs attempting to connect to the KubeMQ server. 5 | /// Specifically this will be thrown when the Ping that is executed on each initial connection fails. 6 | /// 7 | public class UnableToConnectException : Exception 8 | { 9 | internal UnableToConnectException() 10 | : base("Unable to establish connection to the KubeMQ host") { } 11 | } 12 | 13 | /// 14 | /// Thrown when a call is made to an underlying KubeClient after the client has been disposed 15 | /// 16 | public class ClientDisposedException : Exception 17 | { 18 | internal ClientDisposedException() 19 | : base("Client has already been disposed") { } 20 | } 21 | 22 | /// 23 | /// Thrown when an error occurs sending and rpc response 24 | /// 25 | public class MessageResponseTransmissionException : Exception 26 | { 27 | internal MessageResponseTransmissionException(Guid subscriptionID, string messageID, Exception error) 28 | : base($"An error occured attempting to transmit the message response on subscription {subscriptionID} to message id {messageID}", error) { } 29 | } 30 | 31 | internal class NullResponseException : NullReferenceException 32 | { 33 | internal NullResponseException() 34 | : base("null response received from KubeMQ server") { } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Testing/Core/CoreTesting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net10.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/IRecievedMessage.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using System.Diagnostics; 3 | 4 | namespace MQContract.Interfaces 5 | { 6 | /// 7 | /// An interface for describing a Message received on a Subscription to be passed into the appropriate callback 8 | /// 9 | /// The class type of the underlying message 10 | public interface IReceivedMessage 11 | { 12 | /// 13 | /// The unique ID of the received message that was specified on the transmission side 14 | /// 15 | string ID { get; } 16 | /// 17 | /// The message that was transmitted 18 | /// 19 | TMessage Message { get; } 20 | /// 21 | /// The headers that were supplied with the message 22 | /// 23 | MessageHeader Headers { get; } 24 | /// 25 | /// The timestamp of when the message was received by the underlying service connection 26 | /// 27 | DateTime ReceivedTimestamp { get; } 28 | /// 29 | /// The timestamp of when the received message was converted into the actual class prior to calling the callback 30 | /// 31 | DateTime ProcessedTimestamp { get; } 32 | /// 33 | /// The Activity, used for OTel associated with this recieved message 34 | /// 35 | Activity? Activity { get; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Connectors/ActiveMQ/ConsumerInstance.cs: -------------------------------------------------------------------------------- 1 | using Apache.NMS; 2 | 3 | namespace MQContract.ActiveMQ 4 | { 5 | internal sealed class ConsumerInstance(IMessageConsumer messageConsumer, Action cleanup) 6 | : IDisposable 7 | { 8 | private int listenerCount = 1; 9 | private bool disposedValue; 10 | 11 | public void AddListener() => listenerCount++; 12 | 13 | public Task ReceiveAsync() => messageConsumer.ReceiveAsync(); 14 | 15 | public async Task CloseAsync() 16 | { 17 | listenerCount--; 18 | if (listenerCount == 0) 19 | { 20 | await messageConsumer.CloseAsync(); 21 | cleanup(); 22 | } 23 | } 24 | 25 | private void Dispose(bool disposing) 26 | { 27 | if (!disposedValue) 28 | { 29 | if (disposing) 30 | { 31 | messageConsumer.Dispose(); 32 | } 33 | disposedValue=true; 34 | } 35 | } 36 | 37 | ~ConsumerInstance() 38 | { 39 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 40 | Dispose(disposing: false); 41 | } 42 | 43 | public void Dispose() 44 | { 45 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 46 | Dispose(disposing: true); 47 | GC.SuppressFinalize(this); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Connectors/Kafka/Exceptions.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Kafka 2 | { 3 | /// 4 | /// Thrown when a publish message fails to persist in the system 5 | /// 6 | public class PersistenceFailedException : Exception 7 | { 8 | internal PersistenceFailedException() 9 | : base("Persistence Failed") { } 10 | } 11 | 12 | /// 13 | /// Thrown when the service is unable to find a schema for a given message and it is set to fail when missing 14 | /// 15 | public class MissingSchemaException : Exception 16 | { 17 | internal MissingSchemaException(string messageType) 18 | : base($"Unable to load a schema from the registry for the message type {messageType}") { } 19 | } 20 | 21 | /// 22 | /// Thrown when the content of a message fails to validate against the schema 23 | /// 24 | public class SchemaValidationFailedException : Exception 25 | { 26 | internal SchemaValidationFailedException(int schemaId,string messageType) 27 | : base($"The schema with id {schemaId} failed to validate against the message type {messageType}"){} 28 | } 29 | 30 | /// 31 | /// Thrown when the Connection is unable to obtain the Broker MetaData to create a PingResponse 32 | /// 33 | public class UnableToPingException : Exception 34 | { 35 | internal UnableToPingException() 36 | : base("Unable to extract Meta Data from broker to obtain PingResponse") { } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Service/IQueryableMessageServiceConnection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Service 4 | { 5 | /// 6 | /// Used to identify a message service that supports response query style messaging, either through inbox or directly 7 | /// 8 | public interface IQueryableMessageServiceConnection : IMessageServiceConnection 9 | { 10 | /// 11 | /// The default timeout to use for RPC calls when it's not specified 12 | /// 13 | TimeSpan DefaultTimeout { get; } 14 | /// 15 | /// Implements a call to create a subscription to a given channel as a member of a given group for responding to queries 16 | /// 17 | /// The callback to be invoked when a message is received, returning the response message 18 | /// The callback to invoke when an exception occurs 19 | /// The name of the channel to subscribe to 20 | /// The group to bind a consumer to 21 | /// A cancellation token 22 | /// A service subscription object 23 | ValueTask SubscribeQueryAsync(Func> messageReceived, Action errorReceived, string channel, string? group = null, CancellationToken cancellationToken = new CancellationToken()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Connectors/InMemory/Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # MQContract.InMemory 3 | 4 | ## Contents 5 | 6 | - [Connection](#T-MQContract-InMemory-Connection 'MQContract.InMemory.Connection') 7 | - [DefaultTimeout](#P-MQContract-InMemory-Connection-DefaultTimeout 'MQContract.InMemory.Connection.DefaultTimeout') 8 | - [MaxMessageBodySize](#P-MQContract-InMemory-Connection-MaxMessageBodySize 'MQContract.InMemory.Connection.MaxMessageBodySize') 9 | - [TransmissionResultException](#T-MQContract-InMemory-TransmissionResultException 'MQContract.InMemory.TransmissionResultException') 10 | 11 | 12 | ## Connection `type` 13 | 14 | ##### Namespace 15 | 16 | MQContract.InMemory 17 | 18 | ##### Summary 19 | 20 | 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. 21 | 22 | 23 | ### DefaultTimeout `property` 24 | 25 | ##### Summary 26 | 27 | Default timeout for a given QueryResponse call 28 | default: 1 minute 29 | 30 | 31 | ### MaxMessageBodySize `property` 32 | 33 | ##### Summary 34 | 35 | Maximum allowed message body size in bytes 36 | default: 4MB 37 | 38 | 39 | ## TransmissionResultException `type` 40 | 41 | ##### Namespace 42 | 43 | MQContract.InMemory 44 | 45 | ##### Summary 46 | 47 | Thrown when a message transmission has failed within the In Memory system 48 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/CQRSTesting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net10.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Testing/CQRSTesting/ICQRSBaseTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using MQContract.CQRS; 3 | using MQContract.CQRS.Extensions; 4 | using MQContract.Interfaces; 5 | 6 | namespace CQRSTesting 7 | { 8 | [TestClass] 9 | public class ICQRSBaseTests 10 | { 11 | [TestMethod] 12 | public async Task TestInvalidSourceConnection() 13 | { 14 | #region Arrange 15 | var mockConnection = new Mock(); 16 | #endregion 17 | 18 | #region Act 19 | var error = Assert.Throws(() => mockConnection.Object.CreateCQRSConnection()); 20 | #endregion 21 | 22 | #region Assert 23 | Assert.IsNotNull(error); 24 | Assert.IsInstanceOfType(error); 25 | #endregion 26 | 27 | #region Verify 28 | #endregion 29 | } 30 | 31 | [TestMethod] 32 | public async Task TestConnectionDisposal() 33 | { 34 | #region Arrange 35 | var mockConnection = new Mock(); 36 | 37 | mockConnection.Setup(x => x.DisposeAsync()) 38 | .Returns(ValueTask.CompletedTask); 39 | 40 | var cqrsConnection = mockConnection.Object.CreateCQRSConnection(); 41 | #endregion 42 | 43 | #region Act 44 | await cqrsConnection.DisposeAsync(); 45 | #endregion 46 | 47 | #region Assert 48 | #endregion 49 | 50 | #region Verify 51 | mockConnection.Verify(x => x.DisposeAsync(), Times.Once); 52 | #endregion 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Connectors/NATS/Subscriptions/QuerySubscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using NATS.Client.Core; 3 | 4 | namespace MQContract.NATS.Subscriptions 5 | { 6 | internal class QuerySubscription(IAsyncEnumerable> asyncEnumerable, 7 | Func> messageReceived, Action errorReceived) 8 | : SubscriptionBase() 9 | { 10 | protected override async Task RunAction() 11 | { 12 | await foreach (var msg in asyncEnumerable.WithCancellation(CancelToken)) 13 | { 14 | var receivedMessage = ExtractMessage(msg); 15 | try 16 | { 17 | var result = await messageReceived(receivedMessage); 18 | if (result!=null) 19 | await msg.ReplyAsync( 20 | result!.Data.ToArray(), 21 | headers: Connection.ExtractHeader(result), 22 | replyTo: msg.ReplyTo, 23 | cancellationToken: CancelToken 24 | ); 25 | } 26 | catch (Exception ex) 27 | { 28 | errorReceived(ex); 29 | var headers = Connection.ProduceQueryError(ex, receivedMessage.ID, out var responseData); 30 | await msg.ReplyAsync( 31 | responseData, 32 | replyTo: msg.ReplyTo, 33 | headers: headers, 34 | cancellationToken: CancelToken 35 | ); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Connectors/ActiveMQ/Subscriptions/SubscriptionBase.cs: -------------------------------------------------------------------------------- 1 | using Apache.NMS; 2 | using MQContract.Interfaces.Service; 3 | 4 | namespace MQContract.ActiveMQ.Subscriptions 5 | { 6 | internal class SubscriptionBase(Func messageReceived, Action errorReceived, ConsumerInstance consumer) : IServiceSubscription 7 | { 8 | private bool disposedValue; 9 | protected readonly CancellationTokenSource cancelToken = new(); 10 | 11 | internal ValueTask StartAsync() 12 | { 13 | _=Task.Run(async () => 14 | { 15 | while (!cancelToken.IsCancellationRequested) 16 | { 17 | try 18 | { 19 | var msg = await consumer.ReceiveAsync(); 20 | if (msg!=null) 21 | await messageReceived(msg).ConfigureAwait(false); 22 | } 23 | catch (Exception ex) 24 | { 25 | errorReceived(ex); 26 | } 27 | } 28 | }); 29 | return ValueTask.CompletedTask; 30 | } 31 | 32 | public async ValueTask EndAsync() 33 | { 34 | if (!cancelToken.IsCancellationRequested) 35 | await cancelToken.CancelAsync(); 36 | if (consumer!=null) 37 | await consumer.CloseAsync(); 38 | } 39 | 40 | public async ValueTask DisposeAsync() 41 | { 42 | if (!disposedValue) 43 | { 44 | disposedValue=true; 45 | await EndAsync(); 46 | consumer?.Dispose(); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Connectors/InMemory/MessageGroup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading.Channels; 3 | 4 | namespace MQContract.InMemory 5 | { 6 | internal class MessageGroup(Action removeMe) 7 | { 8 | private readonly ConcurrentDictionary> channels = []; 9 | private int index = 0; 10 | 11 | public (Guid id, Channel channel) Register() 12 | { 13 | var id = Guid.NewGuid(); 14 | var result = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader=true, SingleWriter=true }); 15 | channels.TryAdd(id, result); 16 | return (id, result); 17 | } 18 | 19 | public void Unregister(Guid id) 20 | { 21 | channels.TryRemove(id, out _); 22 | if (channels.IsEmpty) 23 | removeMe(); 24 | } 25 | 26 | public async ValueTask PublishMessageAsync(InternalServiceMessage message,CancellationToken cancellationToken) 27 | { 28 | var success = false; 29 | if (index>=channels.Count) 30 | index=0; 31 | if (index 2 | 3 | net8.0;net10.0 4 | enable 5 | enable 6 | $(AssemblyName) 7 | 3.3 8 | https://github.com/roger-castaldo/MQContract 9 | Readme.md 10 | https://github.com/roger-castaldo/MQContract 11 | $(VersionPrefix) 12 | $(VersionPrefix) 13 | Message Queue MQ Contract ServiceBus Messaging Abstraction PubSub QueryResponse 14 | MIT 15 | True 16 | True 17 | True 18 | snupkg 19 | true 20 | $(MSBuildProjectDirectory)\Readme.md 21 | roger-castaldo 22 | $(AssemblyName) 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | Readme.md 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Connectors/ActiveMQ/Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # MQContract.ActiveMQ 3 | 4 | ## Contents 5 | 6 | - [Connection](#T-MQContract-ActiveMQ-Connection 'MQContract.ActiveMQ.Connection') 7 | - [#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)') 8 | - [ActiveMQConnection](#P-MQContract-ActiveMQ-Connection-ActiveMQConnection 'MQContract.ActiveMQ.Connection.ActiveMQConnection') 9 | 10 | 11 | ## Connection `type` 12 | 13 | ##### Namespace 14 | 15 | MQContract.ActiveMQ 16 | 17 | ##### Summary 18 | 19 | This is the MessageServiceConnection implemenation for using ActiveMQ 20 | 21 | 22 | ### #ctor(ConnectUri,username,password) `constructor` 23 | 24 | ##### Summary 25 | 26 | Default constructor for creating instance 27 | 28 | ##### Parameters 29 | 30 | | Name | Type | Description | 31 | | ---- | ---- | ----------- | 32 | | 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 | 33 | | 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 | 34 | | 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 | 35 | 36 | 37 | ### ActiveMQConnection `property` 38 | 39 | ##### Summary 40 | 41 | Underlying connection used to connection to ActiveMQ. Exposed here for additional control if required. 42 | -------------------------------------------------------------------------------- /CQRS/Registrars/MappedConnectionRegistrar.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Consumers; 2 | using MQContract.CQRS.Interfaces; 3 | using MQContract.Interfaces; 4 | using MQContract.Messages; 5 | 6 | namespace MQContract.CQRS.Registrars 7 | { 8 | internal class MappedConnectionRegistrar(IMappedContractConnection mappedConnection) 9 | : IProcessorRegistrar 10 | { 11 | async ValueTask IProcessorRegistrar.RegisterCommandProcessorAsync(CommandConsumer processor, string? group, MessageFilters? messageFilters) 12 | => await mappedConnection.RegisterPubSubAsyncConsumerAsync>( 13 | processor, 14 | group: group, 15 | messageFilters: messageFilters 16 | ); 17 | 18 | async ValueTask IProcessorRegistrar.RegisterCommandProcessorAsync(CommandResponseConsumer processor, string? group, MessageFilters? messageFilters) 19 | => await mappedConnection.RegisterQueryResponseAsyncConsumerAsync>( 20 | processor, 21 | group: group, 22 | messageFilters: messageFilters 23 | ); 24 | 25 | async ValueTask IProcessorRegistrar.RegisterQueryProcessorAsync(QueryResponseConsumer processor, string? group, MessageFilters? messageFilters) 26 | => await mappedConnection.RegisterQueryResponseAsyncConsumerAsync>( 27 | processor, 28 | group: group, 29 | messageFilters: messageFilters 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CQRS/Registrars/ContractedConnectionRegistrar.cs: -------------------------------------------------------------------------------- 1 | using MQContract.CQRS.Consumers; 2 | using MQContract.CQRS.Interfaces; 3 | using MQContract.Interfaces; 4 | using MQContract.Messages; 5 | 6 | namespace MQContract.CQRS.Registrars 7 | { 8 | internal class ContractedConnectionRegistrar(IContractedConnection contractedConnection) 9 | : IProcessorRegistrar 10 | { 11 | async ValueTask IProcessorRegistrar.RegisterCommandProcessorAsync(CommandConsumer processor, string? group, MessageFilters? messageFilters) 12 | => await contractedConnection.RegisterPubSubAsyncConsumerAsync>( 13 | processor, 14 | group: group, 15 | messageFilters: messageFilters 16 | ); 17 | 18 | async ValueTask IProcessorRegistrar.RegisterCommandProcessorAsync(CommandResponseConsumer processor, string? group, MessageFilters? messageFilters) 19 | => await contractedConnection.RegisterQueryResponseAsyncConsumerAsync>( 20 | processor, 21 | group: group, 22 | messageFilters: messageFilters 23 | ); 24 | 25 | async ValueTask IProcessorRegistrar.RegisterQueryProcessorAsync(QueryResponseConsumer processor, string? group, MessageFilters? messageFilters) 26 | => await contractedConnection.RegisterQueryResponseAsyncConsumerAsync>( 27 | processor, 28 | group: group, 29 | messageFilters: messageFilters 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CQRS/Interfaces/Command/ICommandProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.CQRS.Interfaces.Command 2 | { 3 | /// 4 | /// Defines a command processor for the given type of command 5 | /// 6 | /// The type of command 7 | public interface ICommandProcessor : IProcessor 8 | where TCommand : ICommand 9 | { 10 | /// 11 | /// The callback executed against the command 12 | /// 13 | /// The current invocation context for this command instance 14 | /// A cancellation token 15 | /// A ValueTask for async purposes 16 | ValueTask ProcessCommandAsync(ICommandInvocationContext invocationContext,CancellationToken cancellationToken); 17 | } 18 | 19 | /// 20 | /// Defines a command processor for the given type of command that expects a response 21 | /// 22 | /// The type of command 23 | /// The type of response from the command 24 | public interface ICommandProcessor : IProcessor 25 | where TCommand : ICommand 26 | { 27 | /// 28 | /// The callback executed against the command 29 | /// 30 | /// The current invocation context for this command instance 31 | /// A cancellation token 32 | /// The result from the command execution 33 | ValueTask ProcessCommandAsync(ICommandInvocationContext invocationContext, CancellationToken cancellationToken); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Connectors/RabbitMQ/Subscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Interfaces.Service; 2 | using RabbitMQ.Client; 3 | using RabbitMQ.Client.Events; 4 | 5 | namespace MQContract.RabbitMQ 6 | { 7 | internal class Subscription : IServiceSubscription 8 | { 9 | private readonly IChannel channel; 10 | private readonly string consumerTag; 11 | 12 | public static async ValueTask ProduceInstanceAsync(IConnection conn, string channel, string group, Func, ValueTask> messageReceived, Action errorReceived, string? routingKey = null) 13 | { 14 | var connectionChannel = await conn.CreateChannelAsync(); 15 | await connectionChannel.QueueBindAsync(group, channel, routingKey??Guid.NewGuid().ToString()); 16 | await connectionChannel.BasicQosAsync(0, 1, false); 17 | var consumer = new AsyncEventingBasicConsumer(connectionChannel); 18 | consumer.ReceivedAsync+= async (sender, @event) => 19 | { 20 | await messageReceived( 21 | @event, 22 | connectionChannel, 23 | async () => 24 | { 25 | await connectionChannel.BasicAckAsync(@event.DeliveryTag, false); 26 | } 27 | ); 28 | }; 29 | 30 | return new Subscription(connectionChannel, await connectionChannel.BasicConsumeAsync(group, false, consumer)); 31 | } 32 | 33 | private Subscription(IChannel channel, string consumerTag) 34 | { 35 | this.channel=channel; 36 | this.consumerTag=consumerTag; 37 | } 38 | 39 | public async ValueTask EndAsync() 40 | { 41 | await channel.BasicCancelAsync(consumerTag); 42 | await channel.CloseAsync(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Connectors/NATS/Subscriptions/StreamSubscription.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | using NATS.Client.JetStream; 3 | 4 | namespace MQContract.NATS.Subscriptions 5 | { 6 | internal class StreamSubscription(INatsJSConsumer consumer, Func messageReceived, 7 | Action errorReceived) 8 | : SubscriptionBase() 9 | { 10 | protected override async Task RunAction() 11 | { 12 | while (!CancelToken.IsCancellationRequested) 13 | { 14 | try 15 | { 16 | await consumer.RefreshAsync(CancelToken); // or try to recreate consumer 17 | 18 | await foreach (var msg in consumer.ConsumeAsync().WithCancellation(CancelToken)) 19 | { 20 | var success = true; 21 | try 22 | { 23 | await messageReceived(ExtractMessage(msg)).ConfigureAwait(false); 24 | } 25 | catch (Exception ex) 26 | { 27 | success=false; 28 | errorReceived(ex); 29 | await msg.NakAsync(cancellationToken: CancelToken); 30 | } 31 | if (success) 32 | await msg.AckAsync(cancellationToken: CancelToken); 33 | } 34 | } 35 | catch (NatsJSProtocolException e) 36 | { 37 | errorReceived(e); 38 | } 39 | catch (NatsJSException e) 40 | { 41 | errorReceived(e); 42 | // log exception 43 | await Task.Delay(1000, CancelToken); // backoff 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/Service/IInboxQueryableMessageServiceConnection.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.Interfaces.Service 4 | { 5 | /// 6 | /// 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 7 | /// style pub sub where you can specify the destination down to a specific instance. 8 | /// 9 | public interface IInboxQueryableMessageServiceConnection : IQueryableMessageServiceConnection 10 | { 11 | /// 12 | /// Establish the inbox subscription with the underlying service connection 13 | /// 14 | /// Callback called when a message is recieved in the RPC inbox 15 | /// A cancellation token 16 | /// A service subscription object specifically tied to the RPC inbox for this particular connection instance 17 | ValueTask EstablishInboxSubscriptionAsync(Func messageReceived, CancellationToken cancellationToken = new CancellationToken()); 18 | /// 19 | /// Called to publish a Query Request when using the inbox style 20 | /// 21 | /// The service message to submit 22 | /// The unique ID of the message to use for handling when the response is proper and is expected in the inbox subscription 23 | /// A cancellation token 24 | /// The transmission result of submitting the message 25 | ValueTask QueryAsync(ServiceMessage message, Guid correlationID, CancellationToken cancellationToken = new CancellationToken()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Testing/Core/Helper.cs: -------------------------------------------------------------------------------- 1 | using AutomatedTesting.ServiceInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Security.Cryptography; 4 | 5 | namespace AutomatedTesting 6 | { 7 | internal static class Helper 8 | { 9 | private const string ValidCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; 10 | 11 | public static string GenerateRandomString(int length) 12 | => RandomNumberGenerator.GetString(ValidCharacters, length); 13 | 14 | public static IServiceProvider ProduceServiceProvider(string serviceName) 15 | { 16 | var services = new ServiceCollection(); 17 | services.AddSingleton(new InjectedService(serviceName)); 18 | return services.BuildServiceProvider(); 19 | } 20 | 21 | public static ReceivedServiceMessage ProduceReceivedServiceMessage(ServiceMessage message, string? messageTypeID = null, Func? acknowledge = null) 22 | => new(message.ID, messageTypeID??message.MessageTypeID, message.Channel, message.Header, message.Data, acknowledge); 23 | 24 | public static ServiceQueryResult ProduceQueryResult(ServiceMessage? message) 25 | => new(message?.ID??string.Empty, message?.Header??new([]), message?.MessageTypeID??string.Empty, message?.Data?? Array.Empty()); 26 | 27 | private static readonly TimeSpan Delay = TimeSpan.FromMilliseconds(5); 28 | 29 | public static async Task WaitForCount(IEnumerable values, int count, TimeSpan maxTime) 30 | where T : class 31 | { 32 | var task = new Task(() => 33 | { 34 | while (values.Count()=count; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Connectors/KubeMQ/Subscriptions/PubSubscription.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using Microsoft.Extensions.Logging; 3 | using MQContract.KubeMQ.Options; 4 | using MQContract.KubeMQ.SDK.Connection; 5 | using MQContract.KubeMQ.SDK.Grpc; 6 | using MQContract.Messages; 7 | using static MQContract.KubeMQ.SDK.Grpc.Subscribe.Types; 8 | 9 | namespace MQContract.KubeMQ.Subscriptions 10 | { 11 | internal class PubSubscription(ConnectionOptions options, KubeClient client, 12 | Func messageReceived, Action errorReceived, string channel, string group, 13 | StoredChannelOptions? storageOptions, CancellationToken cancellationToken) : 14 | SubscriptionBase(options.Logger, options.ReconnectInterval, client, errorReceived, cancellationToken) 15 | { 16 | private readonly KubeClient Client = client; 17 | 18 | protected override AsyncServerStreamingCall EstablishCall() 19 | { 20 | options.Logger?.LogTrace("Attempting to establish subscription {SubscriptionID} to {Address} on channel {Channel}", ID, options.Address, channel); 21 | return Client.SubscribeToEvents(new Subscribe() 22 | { 23 | Channel = channel, 24 | ClientID = options.ClientId, 25 | Group = group, 26 | SubscribeTypeData = storageOptions == null ? SubscribeType.Events : SubscribeType.EventsStore, 27 | EventsStoreTypeData = (EventsStoreType?)(int?)storageOptions?.ReadStyle ?? EventsStoreType.Undefined, 28 | EventsStoreTypeValue = storageOptions?.ReadOffset ?? 0 29 | }, 30 | options.GrpcMetadata, 31 | cancelToken.Token); 32 | } 33 | 34 | protected override ValueTask MessageReceived(EventReceive message) 35 | => messageReceived(new(message.EventID, message.Metadata, message.Channel, Connection.ConvertMessageHeader(message.Tags), message.Body.ToArray())); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Abstractions/Attributes/QueryMessageAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Attributes 2 | { 3 | /// 4 | /// Use this attribute to specify the Channel, TypeName, TypeVersion, ResponseChannel, DefaultTimeout and/or ResponseType 5 | /// of the given query call. 6 | /// IMPORTANT: The response channel value should either be specified here or on a given query call when the underlying service connection does not support 7 | /// either QueryResponse or Inbox style messaging 8 | /// 9 | /// The channel to be used 10 | /// The query type to use 11 | /// The query type version to use 12 | /// The responce channel to be used when an underlying service connection does not support QueryResponse or Inbox 13 | /// The query response timeout to default to 14 | /// The expected response type for the query 15 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 16 | public class QueryMessageAttribute(string? channel = null, string? typeName = null, string? typeVersion = null, 17 | string? responseChannel=null,int responseTimeoutMilliseconds=60*1000,Type? responseType=null) 18 | : MessageAttribute(channel,typeName,typeVersion) 19 | { 20 | /// 21 | /// The Response Channel defined for the given query 22 | /// 23 | public string? ResponseChannel => responseChannel; 24 | /// 25 | /// The Response Timeout defined for the given query 26 | /// 27 | public TimeSpan ResponseTimeout { get; private init; } = TimeSpan.FromMilliseconds(responseTimeoutMilliseconds); 28 | /// 29 | /// The Response Type defined for the given query 30 | /// 31 | public Type? ResponseType => responseType; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CQRS/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using MQContract.Messages; 2 | 3 | namespace MQContract.CQRS 4 | { 5 | /// 6 | /// Thrown when a command execution call throws an error from the underlying contract connection 7 | /// 8 | public class CommandCallException : Exception 9 | { 10 | /// 11 | /// The error that occured while attempting to execute a given command 12 | /// 13 | public ErrorMessage Error { get; private init; } 14 | 15 | internal CommandCallException(ErrorMessage errorMessage) 16 | : base() 17 | { 18 | Error = errorMessage; 19 | } 20 | } 21 | 22 | /// 23 | /// Thrown when a command call's timeout is exceeded prior to a response being returned 24 | /// 25 | public class CommandTimeoutException : Exception 26 | { 27 | internal CommandTimeoutException(Exception innerException) 28 | : base("Command request timed out", innerException) { } 29 | } 30 | 31 | /// 32 | /// Thrown when a query execution call throws an error from the underlying contract connection 33 | /// 34 | public class QueryCallException : Exception 35 | { 36 | /// 37 | /// The error that occured while attempting to execute a given query 38 | /// 39 | public ErrorMessage Error { get; private init; } 40 | 41 | internal QueryCallException(ErrorMessage errorMessage) 42 | : base() 43 | { 44 | Error = errorMessage; 45 | } 46 | } 47 | 48 | /// 49 | /// Thrown when an invalid contract connection type is supplied in an attempt to create a CQRS connection 50 | /// 51 | public class InvalidConnectionException : InvalidCastException 52 | { 53 | internal InvalidConnectionException(Type type) 54 | : base($"The type of {type.FullName} is not a valid connection type to use with CQRS") { } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Connectors/GooglePubSub/Subscription.cs: -------------------------------------------------------------------------------- 1 | using Google.Cloud.PubSub.V1; 2 | using MQContract.Interfaces.Service; 3 | using MQContract.Messages; 4 | 5 | 6 | namespace MQContract.GooglePubSub 7 | { 8 | internal class Subscription(SubscriberServiceApiClient subscriberClientApi, SubscriptionName subscriptionName, Func messageReceived, Action errorReceived, string channel) : IServiceSubscription, IAsyncDisposable 9 | { 10 | protected readonly CancellationTokenSource cancelToken = new(); 11 | private bool disposedValue; 12 | 13 | public void Start() 14 | { 15 | _ = Task.Run(async () => 16 | { 17 | while (!cancelToken.IsCancellationRequested) 18 | { 19 | try 20 | { 21 | var msg = await subscriberClientApi.PullAsync(subscriptionName, 1, cancelToken.Token); 22 | if (msg!=null) 23 | await messageReceived(Connection.ConvertMessage( 24 | msg.ReceivedMessages[0], 25 | channel, 26 | async () => await subscriberClientApi.AcknowledgeAsync(subscriptionName, [msg.ReceivedMessages[0].AckId], cancelToken.Token) 27 | )).ConfigureAwait(false); 28 | } 29 | catch (Exception ex) 30 | { 31 | errorReceived(ex); 32 | } 33 | } 34 | }); 35 | } 36 | 37 | public async ValueTask EndAsync() 38 | { 39 | if (!cancelToken.IsCancellationRequested) 40 | await cancelToken.CancelAsync(); 41 | } 42 | 43 | public async ValueTask DisposeAsync() 44 | { 45 | if (!disposedValue) 46 | { 47 | disposedValue=true; 48 | await EndAsync(); 49 | cancelToken.Dispose(); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Abstractions/Interfaces/IContractMetric.cs: -------------------------------------------------------------------------------- 1 | namespace MQContract.Interfaces 2 | { 3 | /// 4 | /// Houses a set of metrics that were requested from the internal metric tracker. 5 | /// All message conversion durations are calculated from the perspective: 6 | /// - 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 7 | /// - 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 8 | /// 9 | public interface IContractMetric 10 | { 11 | /// 12 | /// Total number of messages 13 | /// 14 | ulong Messages { get; } 15 | /// 16 | /// Total amount of bytes from the messages 17 | /// 18 | ulong MessageBytes { get; } 19 | /// 20 | /// Average number of bytes from the messages 21 | /// 22 | ulong MessageBytesAverage { get; } 23 | /// 24 | /// Minimum number of bytes from the messages 25 | /// 26 | ulong MessageBytesMin { get; } 27 | /// 28 | /// Maximum number of bytes from the messages 29 | /// 30 | ulong MessageBytesMax { get; } 31 | /// 32 | /// Total time spent converting the messages 33 | /// 34 | TimeSpan MessageConversionDuration { get; } 35 | /// 36 | /// Average time to encode/decode the messages 37 | /// 38 | TimeSpan MessageConversionAverage { get; } 39 | /// 40 | /// Minimum time to encode/decode a message 41 | /// 42 | TimeSpan MessageConversionMin { get; } 43 | /// 44 | /// Maximum time to encode/decode a message 45 | /// 46 | TimeSpan MessageConversionMax { get; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /BenchMark/InMemoryBenchmarks/SubscribingInMemory.cs: -------------------------------------------------------------------------------- 1 | using BenchMark.Messages; 2 | using BenchmarkDotNet.Attributes; 3 | using MQContract; 4 | using MQContract.Interfaces; 5 | 6 | namespace BenchMark.InMemoryBenchmarks 7 | { 8 | [MemoryDiagnoser] 9 | public class SubscribingInMemory 10 | { 11 | private const string channel = "Announcements"; 12 | private static readonly Announcement testMessage = new("The quick brown fox"); 13 | 14 | [Params(1, 10, 100, 1000, 5000, 10000, 50000)] 15 | public int MessageCount { get; set; } 16 | private IContractedConnection? contractConnection; 17 | private TaskCompletionSource? completionSource; 18 | private ISubscription? subscription; 19 | 20 | [IterationSetup] 21 | public void SetupIteration() 22 | { 23 | var count = MessageCount; 24 | contractConnection = ContractConnection.Instance(new MQContract.InMemory.Connection()); 25 | completionSource = new(); 26 | var subTask = contractConnection.SubscribeAsync( 27 | (message) => 28 | { 29 | count--; 30 | if (count<=0) 31 | completionSource.TrySetResult(); 32 | return ValueTask.CompletedTask; 33 | }, 34 | (err) => { }, 35 | channel:channel 36 | ).AsTask(); 37 | subTask.Wait(); 38 | subscription = subTask.Result; 39 | } 40 | 41 | [IterationCleanup()] 42 | public void CleanupIteration() 43 | { 44 | subscription?.EndAsync().AsTask().Wait(); 45 | contractConnection?.CloseAsync().AsTask().Wait(); 46 | } 47 | 48 | [Benchmark] 49 | public async Task ReadAllSubscriptionMessages() 50 | { 51 | var count = MessageCount; 52 | for (var x = 0; x(testMessage,channel: channel); 54 | await completionSource!.Task; 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------