Releases
ServiceConnect is versioned per-package. Each package ships independently on NuGet; GitHub Releases mark the tagged commits those packages were built from. If you want to know “what changed between v5 and v6 of ServiceConnect.Interfaces”, you’re in the right place.
Where releases live
Section titled “Where releases live”- GitHub Releases — github.com/R-Suite/ServiceConnect-CSharp/releases. Each entry lists the tag, the commit, and a summary of the changes in that cut.
- NuGet packages — published under the
ServiceConnect.*prefix. The core packages are:ServiceConnect— the bus, hosted service, and dispatch machinery.ServiceConnect.Interfaces— the public API surface (IBus, configuration, handlers).ServiceConnect.Client.RabbitMQ— the RabbitMQ transport.ServiceConnect.Persistence.MongoDb— MongoDB persistence for process managers and aggregators.ServiceConnect.Persistence.InMemory— in-process persistence for tests and samples.ServiceConnect.Telemetry— OpenTelemetry instrumentation hooks.ServiceConnect.HealthChecks—IHealthCheckintegrations forMicrosoft.Extensions.Diagnostics.HealthChecks.
Picking a version
Section titled “Picking a version”Match the major versions of the packages you reference. ServiceConnect 7.x expects ServiceConnect.Interfaces 7.x; the transport and persistence packages follow the same major line. Mixing majors is unsupported and will surface as binding redirects or missing-method exceptions.
For new applications, take the latest stable of each package. For existing applications, read the release notes for any major version bumps — we call out breaking changes in the GitHub Release body.
v7 is a clean-architecture rewrite. The legacy static Bus is gone; configuration is fluent and DI-wired; every public path is async with CancellationToken; the wire format is System.Text.Json; OpenTelemetry instrumentation is operator-grade; and the solution is consolidated to seven shipping packages. It is not source- or binary-compatible with v6 — read this entry before upgrading.
What v7 ships
Section titled “What v7 ships”- Target frameworks:
net8.0andnet10.0on every package.net8.0will be dropped in the first major after Microsoft’s EoL (November 10, 2026). - Packages (all version 7.0.0):
ServiceConnect,ServiceConnect.Interfaces,ServiceConnect.Client.RabbitMQ,ServiceConnect.Persistence.MongoDb,ServiceConnect.Persistence.InMemory,ServiceConnect.Telemetry, and the newServiceConnect.HealthChecks. - Dropped from the solution:
ServiceConnect.Persistence.SqlServer,ServiceConnect.Persistence.Redis,ServiceConnect.Filters.MessageDeduplication, and a long tail of internal/experimental projects (17 production projects collapsed to seven). - License: MIT (relicensed from the prior more-restrictive license).
- SourceLink +
.snupkgship on every package; step-into works from any consumer.
Breaking changes
Section titled “Breaking changes”Bus, configuration, and handlers
Section titled “Bus, configuration, and handlers”- Static
Busis gone. UseAddServiceConnect(...)+ the fluentServiceConnectBuilderfrom DI.IBusisIAsyncDisposable; once stopped or disposed, the instance is latched — restart requires a new instance from DI. - Async-first everywhere.
IBus,IProducer,IConsumer,IConsumeContext,IProcessManagerFinder,ITimeoutStore,IAggregatorPersistor, middleware, and filters all returnTask/ValueTaskand take aCancellationToken. The sync-over-async paths in the RabbitMQ transport have been removed. - Handler signatures take the context as a parameter.
IMessageHandler<T>.HandleAsync(T message, IConsumeContext ctx, CancellationToken ct)(and the equivalents onIProcessHandler<T>/IStreamHandler<T>) replace the previous settableContext/Streamproperties, which were unsafe for singleton handlers. - Public surface tightened to extension contracts only.
Bus,MessageDispatcher, every concrete persistor, every concrete RabbitMQ class, every*Configurationtype, and all hosted services are nowinternal. Public API is interfaces inServiceConnect.Interfacesplus the builder/extension methods on the bus configuration root. ConfigurePipeline(Action<PipelineConfiguration>)is internal. Use the typed builders:AddOutgoingFilter<T>,AddBeforeConsumingFilter<T>,AddOnConsumedSuccessfullyFilter<T>,AddAfterConsumingFilter<T>,AddSendMessageMiddleware<T>,AddMessageProcessingMiddleware<T>, plus outermost-position helpers.- Request fan-out is explicit.
SendOptions.EndPointsandRequestOptions.EndPointsare removed; usebus.SendToManyAsync(msg, ["a","b"])for fan-out andPublishRequestAsyncfor broadcast request.SendRequestMultiAsyncwith a positiveExpectedReplyCountnow throwsRequestTimeoutException(withPartialReplies) instead of silently returning under-delivery. PublishOptionsis areadonly record struct;RequestOptionsrequiresRequestOptions.Default. Mutable-shared-instance races are closed;default(RequestOptions)is rejected so the parameterless ctor’s defaults always apply.- Read-only collections on the wire.
TimeoutData.Headers,ConsumeEventArgs.Headers,TimeoutsBatch.DueTimeouts,IBus.RouteAsyncdestinations,IConsumer.StartConsumingAsyncmessage-type lists, andIMessageDispatcher.DispatchAsyncheaders are nowIReadOnly*. Mutating middleware constructs a new instance. IConsumeContext.ReplyAsyncusesReplyOptionsin place of(message, headers, ct).IBusConfiguration.ExceptionHandlerisFunc<Exception, CancellationToken, ValueTask>?(wasAction<Exception>?).IRegistryInitializeris internal, andIProducer.DisconnectAsyncis removed — dispose viaIAsyncDisposable.DisposeAsync(or rely on DI).
Transport
Section titled “Transport”- TLS is on by default.
SslEnabled = true(AMQPS port 5671). For local plaintext dev, sett.SslEnabled = false; a non-loopback plaintext connection logs aWarningyou can suppress withSuppressPlaintextWarning. - Publisher confirms are on by default. Every publish waits for a broker ack before returning. Benchmark throughput-sensitive paths; configuring
PublisherAcknowledgements=falsetogether with a finitePublishTimeoutis now a startup error. IProducerbody isReadOnlyMemory<byte>; headers areIReadOnlyDictionary<string,string>?.byte[]converts implicitly; anullbody is no longer expressible — useReadOnlyMemory<byte>.Empty.- External
IConsumerimplementations must addIsCancelledByBroker(defaultfalse); the bus uses it to flipIsConsumingtofalsewhen the broker has cancelled the consumer (queue deleted, policy expired, mirror promoted). MessageTypeExchangeNamehash droppedAssemblyQualifiedName. Exchanges are derived fromtype.FullName + assembly.GetName().Name— version-stable. Single-source apps see no change; apps with broker-shared version skew will see new exchange names on first v7 deploy.- Retry and error publishes use
mandatory:true. Topology gaps that pre-v7 silently dropped messages now surface asPublishException— fix the topology rather than relying on the drop.
Persistence
Section titled “Persistence”IHasCorrelationIdis required for aggregator data types. Replaces v6’s reflection-based discovery.Messagealready implements it; custom non-Messagedata types now fail to compile until they implement it.IAggregatorPersistortightened.InsertDataAsync(IHasCorrelationId data, …);GetDataAsyncreturnsTask<IReadOnlyList<IHasCorrelationId>>.AggregatorSnapshot.ResolvedMessagesmatches.- Mongo aggregator
Namepartition value changed fromtypeof(Aggregator<T>).FullNametotypeof(ConcreteAggregator).FullName. Existing rows must be renamed before restarting on v7 or they will be invisible and accumulate. - Generic saga collection names sanitized —
+/ backtick /[/]/,in generic-typeFullNamebecome_. Rename existing generic-saga collections withrenameCollectionbefore deploy. - MongoDB.Driver bumped to 3.8.0 (from 2.23.1). The bundled persistor handles
GuidRepresentationMode = V3andRenderArgs<T>transparently; consumers usingMongoDB.Driverdirectly should follow the official 2.x→3.x guide. - Startup guards on Mongo persistence:
WriteConcern.Unacknowledged(w:0) is rejected; conflicting Guid-serializer registrations are rejected; the legacy(Locked, Time)timeout index is dropped and(Time, Locked, LockExpiresAt)is created. - System.Text.Json replaces Newtonsoft.Json in all shipped packages. Newtonsoft is no longer transitive; add an explicit reference if you relied on transitive flow. The wire format is JSON-equivalent for typical messages but STJ rejects
NaN/Infinitydoubles, nesting > 32 levels, trailing commas, and JS-style comments. ASerializationCompatTestsproject enforces v6↔v7 round-trip on every PR. IMessageSerializerreduced to four methods:Serialize<T>(T, IBufferWriter<byte>),Deserialize<T>(ReadOnlyMemory<byte>),Deserialize(ReadOnlyMemory<byte>, Type), and a defaultReadOnlySequence<byte>overload. Thebyte[]/ReadOnlySpan<byte>overloads are gone.TimeoutData.Destinationis nullable.IKeyValueStore/ICacheProviderswapGet<>forTryGet<>to distinguish “absent” from “present-with-null”.
Streaming
Section titled “Streaming”- Body type cascades to
ReadOnlyMemory<byte>.IMessageBusReadStream.WriteandIMessageBusWriteStream.WriteAsyncno longer take(byte[], offset, count); passbuf.AsMemory(0, len)instead. - Streams latch on first transport failure. A faulted write makes the stream unusable; create a fresh stream to recover. Caps:
MaxStreamSizeBytes,MaxActiveStreams.
Telemetry
Section titled “Telemetry”- No more static telemetry state.
ServiceConnectActivitySource.Options/MessagingSystemAttributesare gone; instrumentation methods take options + attributes as parameters, so two buses in one process can have independent enrichment. - One ActivitySource:
"ServiceConnect.Telemetry.Bus"(was three). Per-direction toggles viaEnablePublishTelemetry/EnableConsumeTelemetry/EnableSendTelemetry. - OTel semconv 1.x messaging attributes. Spans/metrics emit
messaging.operation.type(publish/process) andmessaging.operation.name; the pre-1.xmessaging.operationattribute is gone.messaging.destination.namereflects the broker exchange/routing key, not the CLR type’sFullName. Dashboards filtering on the legacy attributes must update.
What’s new
Section titled “What’s new”Bus and pipeline
Section titled “Bus and pipeline”OnConsumedSuccessfullyfilter stage — fourth pipeline stage that runs only after a successful handler invocation. The canonical building block for at-most-once side effects and for the dedupe pattern that replaces the removedMessageDeduplicationpackage.- DI-first builder. Single
AddServiceConnectcall perIServiceCollection; re-entry is rejected. Multi-bus is achieved via separate service collections. Health-check probing for multiple buses is available viaAddServiceConnectBus/AddServiceConnectConsumer/AddServiceConnectProduceronIHealthChecksBuilder. - Configurable caps.
MaxInflightRequests,MaxStreamSizeBytes,MaxActiveStreams,MaxRoutingSlipHops,DisposeTimeout. - Lifecycle flags.
AllowMissingProducer(consume-only buses),DeadLetterUnhandledMessages(explicit error-queue routing),StrictReplyValidation(rejects the cross-bus fallback that a queue-name-aware producer could spoof),IncludeMachineNameInHeaders(defaultfalseto avoid hostname leakage on shared brokers). IBus.RequestTimeoutAsync(correlationId, delay, ct)for scheduling saga timeouts.IBus.IsCancelledByBroker/IBus.IsStopped/IConsumer.IsStopped/IProducer.HasAttemptedConnection/IProducer.IsHealthy/IProducer.GetHealthSnapshot()for health-check semantics.RequestSendCancelledException(inheritsOperationCanceledException) distinguishes send-pipeline cancel from caller-token cancel and from timeout.- Native streaming.
bus.CreateStream<T>(endpoint)→IMessageBusWriteStreamwith zero-copy write path;IStreamHandler<T>.ExecuteAsync(IMessageBusReadStream stream, ...). - Polymorphic dispatch with type-hierarchy walking. New processor chain (
HandlerProcessor,ReplyProcessor,ProcessManagerProcessor,AggregatorProcessor,StreamProcessor) backed by pluggable registries — no more per-message reflection.
Transport
Section titled “Transport”PublishOptions.RoutingKeyis honoured by the RabbitMQ transport (withIProducer.SupportsRoutingKeyas a capability flag for third-party transports).RabbitMqOptions.MaxPublishWaitTimecaps the wall-clock budget for publish-confirm retries.MessageIdsurvives publish retries. Constructed once per public call and captured into the retry closure — consumer-side dedupe under retries is now meaningful.TimeoutExceptionis retriable under the at-least-once publish contract: a publish-confirm timeout drives a channel reset and re-submits the sameBasicProperties.
Persistence
Section titled “Persistence”IProcessManagerTypeRegistry(inServiceConnect.Interfaces) enumerates registered saga data types. The Mongo persistor uses it to pre-createCorrelationIdunique indexes via a hosted service at startup — closes the cross-process startup race.IIdentified(Guid Idgetter) onMemoryData<T>andMongoDbData<T>for stable identity across stores.IAggregatorPersistor.CountResolvedAsyncandReleaseSnapshotAsync— additive default-interface-method members for cheap typed-count gating and lease release.
Observability and operations
Section titled “Observability and operations”ServiceConnect.HealthCheckspackage withBusConsumingHealthCheck,ConsumerConnectionHealthCheck, andProducerConnectionHealthCheck. ConfigurablerecoveryGraceWindow(default 30s) absorbs transient broker flap; permanent broker-cancel bypasses the grace.- Operator-grade metrics via
System.Diagnostics.Metrics. Four OTelmessaging.*instruments (publish/process duration histograms + published/consumed counters) plus ServiceConnect counters (retry.attempts,retry.drops,publish.confirm_timeouts,audit.drops,outgoing_filters.blocked) and an in-flight UpDownCounter. - W3C
traceparent/tracestateinjected on outgoing messages (already extracted on consume — end-to-end distributed tracing works out of the box). MaxTagValueLength(default 256) bounds user-controlled string tags for cardinality safety.ExceptionMessageSanitiserredacts PII in exception messages before they reach activity status andexception.messageevent tags.- Connection-lifecycle Info logs (
ConnectionOpened/ProducerConnectionOpened/ConnectionRecovered/ConnectionLost) carry the connection’sMessageId.
Hardening
Section titled “Hardening”The bulk of the v7 work landed under hardening: trust boundaries on inbound headers, caps against amplification, persistence-lease correctness under cancellation and reaper paths, and a long tail of races in lifecycle, recovery, dispatch, and disposal.
- Server-authoritative wire headers.
DestinationAddress,MessageId,MessageType,TypeName, andFullTypeNameare stamped on outbound; reply routing resolves through registered handlers only (noType.GetTypeon caller-supplied wire headers).StrictReplyValidationopts into the tightest mode. - Cardinality and DoS caps. Message-size, header-count, header-size, routing-slip-hop, active-stream, in-flight-request, packet-number, and stream-size caps. Recursive byte budget on AMQP table/array nested header values. Gzip magic-byte check + decompression size cap.
- Newtonsoft → STJ removes the
TypeNameHandling.Autodeserialisation-gadget surface inDeepCloneand elsewhere. - Audit-publish failures no longer trigger redelivery. Audit is a success-side effect; malformed
RetryCountheaders route to the error queue rather than silently resetting the retry budget; unresolved message types are terminal. - Bus lifecycle is single-use and DI-owned.
Busno longer disposes transport singletons;StartConsumingAsyncafter stop is latched;DisposeAsyncis bounded byDisposeTimeout; the lifecycle semaphore won’t deadlock on shutdown. - Mongo timeout-store correctness. Candidate ordering tie-broken by
Id(eliminates starvation under saturation); cancellation betweenUpdateManyandFindAsyncperforms best-effort lease cleanup;$facetaggregation removes the second round-trip. - Aggregator correctness.
RemoveDataAsyncdistinguishesKeyNotFoundException(structural) fromConcurrencyException(race); batch-size flushes gate on resolved-count (no more spinning on unresolved-only batches);OnTimerFiredregisters-before-rechecks to close a disposal-snapshot race. - In-memory persistence no longer drops state at 2 days; deep-clones on read; types are
IDisposableso DI rebuilds don’t leak timers. - Consumer-host recovery. Auto-recovery refreshes the consumer tag via
ConsumerTagChangeAfterRecoveryAsync; staleUnregisteredAsyncevents whose tag has been superseded by recovery are ignored; queues are bound beforeBasicConsume(per-channel serialisation contract). - Telemetry correctness. Enricher
OperationCanceledExceptionno longer leaks an activity; body copy is gated on listener presence; tag values are length-bounded; exception messages are sanitised. - Saga / process-manager. Poison rows wrap to
PersistenceExceptionand route to error queue rather than NACK-looping; saga predicates handle explicit-interface property impls (Mongo + InMemory);ProcessManagerHandlerRegistrydedupes byHandlerTypeto handle dual-interface sagas and repeat-assembly scans.
Removed
Section titled “Removed”ServiceConnect.Filters.MessageDeduplicationpackage — the old implementation silently dropped legitimate broker redeliveries and shared in-memory state via a static field. No replacement; use the newOnConsumedSuccessfullystage for per-consumer dedupe. See theCustomFilterAndMiddlewareexample.ServiceConnect.Persistence.SqlServerandServiceConnect.Persistence.Redis— no replacement.- Static
Bus— useAddServiceConnect(...)from DI. IProducer.DisconnectAsync— dispose viaIAsyncDisposable.DisposeAsync.SendOptions.EndPointsandRequestOptions.EndPoints— useSendToManyAsync/PublishRequestAsync.SendEventArgs.EndPoints(plural) — multi-endpoint sends raise one event per destination; correlate byCorrelationId.IProcessMessageMiddleware— superseded byIMessageProcessingMiddleware.Get<TKey,TValue>onIKeyValueStore/ICacheProvider— useTryGet.byte[]/ReadOnlySpan<byte>overloads onIMessageSerializer— use theROM<byte>/ROSequence<byte>shape.- Pre-1.x
messaging.operationOTel attribute — replaced bymessaging.operation.type+messaging.operation.name. - Newtonsoft.Json from all production packages.
Migration
Section titled “Migration”For application code, the upgrade path is roughly:
- Replace static
Bus.Initialize(...)withAddServiceConnect(builder => ...)in your composition root; resolveIBusfrom DI. - Convert handlers to
HandleAsync(T message, IConsumeContext ctx, CancellationToken ct); drop any settableContext/Streamproperty usage. - Replace
SendOptions.EndPointsandRequestOptions.EndPointsusage withSendToManyAsync/PublishRequestAsync/SendRequestMultiAsync. - Convert pipeline registration to the typed
AddOutgoingFilter<T>/AddBeforeConsumingFilter<T>/AddOnConsumedSuccessfullyFilter<T>/AddAfterConsumingFilter<T>/AddSendMessageMiddleware<T>/AddMessageProcessingMiddleware<T>builders. - Set
t.SslEnabled = falseexplicitly for plaintext local dev; keep TLS on for everything else. - If you ship custom non-
Messageaggregator data, implementIHasCorrelationIdon it. - If you query
Aggregator<T>-named partitions or generic-saga collections directly, run the rename scripts before deploying. - If your dashboards filter on the old
messaging.operationattribute or onmessaging.destination.name = "<CLR type FullName>", update them.
For full per-commit context, see the v7 release on GitHub and the git log master..v7.0.0 range.
Source and tags
Section titled “Source and tags”Every release corresponds to a git tag. Browse the source at a specific version by visiting https://github.com/R-Suite/ServiceConnect-CSharp/tree/<tag>. The master branch is the development head; tagged releases are the frozen points that produced the NuGet artifacts.