Add System.ServiceModel.Msmq — client-side MSMQ transport - #5958
Open
afifi-ins wants to merge 3 commits into
Open
Add System.ServiceModel.Msmq — client-side MSMQ transport#5958afifi-ins wants to merge 3 commits into
afifi-ins wants to merge 3 commits into
Conversation
afifi-ins
added a commit
to afifi-ins/wcf
that referenced
this pull request
Jun 8, 2026
The Helix Linux and macOS legs were failing on three theory cases:
MsmqConditionsTest.ImplicitDtcEnabled_HonorsEnvVar("true"|"TRUE"|"True")
Symptom:
Assert.True() Failure
Expected: True
Actual: False
Root cause: ConditionalTestDetectors.IsImplicitDtcEnabled() is
Windows-only by design (MSMQ itself is Windows-only) and returns
false on non-Windows hosts regardless of the env var. The
[SupportedOSPlatform("windows")] attribute on the test class is an
analyzer hint only — xunit still ran the tests on Linux / macOS Helix
queues and the Assert.True expectation no longer held.
Fix: assert the actual contract — true on Windows, false elsewhere —
so the test passes on every platform the dispatch runs on.
Verified locally on Windows: 150/150 unit tests pass.
Port the WCF MSMQ client transport (NetMsmqBinding and MsmqIntegrationBinding) from .NET Framework into a new, shippable System.ServiceModel.Msmq NuGet package. Enables modern .NET clients to send SOAP and raw-body messages to MSMQ queues, with optional DTC transaction enlistment. The package is Windows-only and self-contained on the managed side (depends only on System.ServiceModel.Primitives). Features - NetMsmqBinding: SOAP-over-MSMQ with the full binding-element graph (MsmqTransportBindingElement, MsmqBindingElementBase, MsmqBindingBase). - MsmqIntegrationBinding: interop with legacy MSMQ applications via MsmqMessage<T> and MsmqIntegrationMessageProperty (raw body, no SOAP envelope). - Channel shapes: IOutputChannel and IOutputSessionChannel with their channel factories; MsmqUri address translators (net.msmq://, FormatName, private/direct). - Native interop: a hand-rolled P/Invoke layer over mqrt.dll (UnsafeNativeMethods, SafeMsmqQueueHandle, NativeMsmqMessage, MsmqQueue) with no third-party runtime dependency. - Transactions: MessageQueueTransactionType None/Single/Automatic; Automatic enlists the ambient System.Transactions transaction into MSMQ's native DTC via DtcTransactionBridge (no custom IEnlistmentNotification required). - Error handling: MsmqException maps native MQ_ERROR_* HRESULTs to the corresponding WCF exceptions. - MSMQ-Integration enums (AcknowledgeTypes, Acknowledgment, MessageType, MessagePriority) shipped by this package with values pinned to the native MQMSG_* constants. - Security value types: NetMsmqSecurity, MsmqTransportSecurity, MessageSecurityOverMsmq, MsmqAuthenticationMode, MsmqEncryptionAlgorithm, MsmqSecureHashAlgorithm. Shared-assembly change - Promote AddressAccessDeniedException from System.ServiceModel.NetNamedPipe to System.ServiceModel.Primitives so the MSMQ transport can map MQ_ERROR_ACCESS_DENIED / MQ_ERROR_SHARING_VIOLATION to it without a cross-transport coupling. NetNamedPipe keeps a [TypeForwardedTo] for binary compatibility. Testing - 213 unit tests (binding shape, URI translation, exception mapping, native message layout, enum values, transaction-mode dispatch). - 5 scenario tests against a real localhost MSMQ queue manager (Binding.Msmq.IntegrationTests); 2 are environment-gated for DTC. - Helix CI wired to best-effort enable the Windows MSMQ feature; scenarios skip cleanly via [Condition(MsmqInstalled)] when MSMQ is unavailable. - Localized string resources (13 languages); package validation passes with an empty CompatibilitySuppressions.xml. Scope: client send path only. Service hosting / receive pipeline is out of scope (see CoreWCF). Session-gram framing for IOutputSessionChannel is deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cffba073-486d-49e6-8683-cf30c1eb5e28
afifi-ins
force-pushed
the
feature/msmq-porting
branch
from
July 24, 2026 04:14
5a373f1 to
64371d9
Compare
Correctness fixes in the native send path: * MQPROPVARIANT is 16 bytes on x86 and 24 on x64. PropVariantSize was hard-coded to 24, which over-strode every property slot on 32-bit and handed MSMQ a misaligned aPropVar array. * PROPID_M_EXTENSION, PROPID_M_EXTENSION_LEN and PROPID_M_BODY_TYPE used the wrong values (24/25/36 instead of 35/36/42); 24 and 25 collided with PROPID_M_AUTH_LEVEL and PROPID_M_AUTHENTICATED. * ResponseQueue wrote PROPID_M_RESP_QUEUE; .NET Framework writes PROPID_M_RESP_FORMAT_NAME, which is the slot that accepts a format name. Administration and response queues now resolve through MsmqUri.FormatNameAddressTranslator as netfx does. * Four MQ_ERROR_* constants were wrong, two of which aliased unrelated real errors (0xC00E0026 is PRIVILEGE_NOT_HELD, not QUEUE_NOT_AVAILABLE; 0xC00E0051 is TRANSACTION_SEQUENCE, not TRANSACTION_IMPORT). All values verified against the Windows SDK Mq.h, and 14 missing security/DTC codes were added so they normalize to CommunicationException. * MQOpenQueue now marshals straight into a SafeMsmqQueueHandle instead of an out IntPtr plus SetHandle, closing a handle-leak window. Binding settings that were stored but never reached the wire: * Durable now writes PROPID_M_DELIVERY. Without it MSMQ applied its EXPRESS default, so every message a caller believed was durable was lost on a queue manager restart. * UseSourceJournal, DeadLetterQueue, CustomDeadLetterQueue and UseMsmqTracing now write PROPID_M_JOURNAL, PROPID_M_DEADLETTER_QUEUE and PROPID_M_TRACE. * MsmqTransportSecurity now writes PROPID_M_AUTH_LEVEL, PROPID_M_PRIV_LEVEL, PROPID_M_HASH_ALG and PROPID_M_ENCRYPTION_ALG. * QueueTransferProtocol now selects the SRMP/SRMPS address translators; MsmqTransportBindingElement.AddressTranslator is no longer a null stub. * The send timeout is enforced as a budget around open plus send instead of being discarded. Configurations that silently did the wrong thing now fail loudly: * NetMsmqSecurityMode.Message and Both threw nothing and emitted no SecurityBindingElement, putting plaintext on the wire under a binding that claimed message security. They now throw NotSupportedException. * UseActiveDirectory was ignored, addressing a different queue than the caller asked for. It now throws NotSupportedException. * MsmqMessageSerializationFormat.Binary and ActiveX depend on BinaryFormatter and the ActiveX serializer and throw PlatformNotSupportedException instead of emitting an unreadable body. * Off-Windows use throws PlatformNotSupportedException rather than DllNotFoundException for mqrt. MsmqIntegrationBinding could not send at all: * The binding contributed no encoder, so the factory fell back to a SOAP 1.2 binary encoder while advertising MessageVersion.None and every send threw on the version mismatch. The integration path now carries no encoder and serializes MsmqIntegrationMessageProperty.Body through the binding's SerializationFormat, matching netfx. * MsmqIntegrationMessageProperty.Priority and TimeToReachQueue are validated on set. A negative TimeToReachQueue previously clamped to zero seconds, which makes MSMQ discard the message immediately. Structure and lifecycle: * The three output channels and three factories duplicated the whole encode/send pipeline and had already diverged. They now share MsmqOutputChannelBase and MsmqOutputChannelFactoryBase so a fix lands once rather than three times. * Transaction.Current is captured on the calling thread and passed down explicitly. BeginSend dispatched through Task.Run and read the thread-static ambient transaction on the worker thread, where it was always null, so asynchronous sends committed outside the caller's transaction. * Address translation moved from the channel constructor to Open, so CreateChannel returns a channel and Open reports a bad address. * Encoding uses MaxReceivedMessageSize as the quota rather than int.MaxValue, and factory cleanup funnels through one method instead of running on both OnClose and OnEndClose. * Factory constructors validate the BindingContext before the base constructor dereferences it, so a null argument raises ArgumentNullException rather than NullReferenceException. Security hardening: * Percent-decoded queue paths are rejected when they contain NUL, ',', ';' or control characters. An encoded NUL truncated the format name during marshaling and could redirect a send to another queue. Tests: * Two tests pinned bugs as correct behaviour: one asserted the 24-byte MQPROPVARIANT and one asserted the integration binding emits no encoder. Both now assert the correct contract. * Added coverage for the transport properties, the security propids, the algorithm mappings, the integration serializer, address translator selection, durable delivery, asynchronous transactional sends and send timeouts. * Scenario cleanup catches MessageQueueException instead of everything. CI: * The Helix Windows pre-command embedded a literal ';', which splits the HelixPreCommands list mid-command; cmd then reported "'try' is not recognized" and the work item timed out. It no longer installs the MSMQ Windows feature per work item, which was a multi-minute DISM operation that exhausted the timeout, and only starts the service when present. Style and housekeeping: * Stale comments describing the removed MSMQ.Messaging reflection approach, plus comments claiming features that already ship. * Renamed the MsmqBindingBase transport field to _transport, replaced var with explicit types, used nameof for TChannel, converted trivial properties to auto-properties, and normalized IsDefined accessibility. * Removed three unused resource strings and regenerated the xlf files. * Restored ValidityDuration and the missing DefaultValue attributes for .NET Framework surface parity. * README documents the configurations that throw and the properties that only affect the receive side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cffba073-486d-49e6-8683-cf30c1eb5e28
CA1416 is warn-as-error in CI: the serializer reads
MsmqIntegrationMessageProperty.Body, which is annotated
SupportedOSPlatform("windows"), so the serializer must carry the same
annotation. Local incremental builds masked this because the project was
already up to date from an earlier build that did not pass -warnaserror.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cffba073-486d-49e6-8683-cf30c1eb5e28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a new client transport package,
System.ServiceModel.Msmq, thatprovides the WCF client-side surface of .NET Framework's
NetMsmqBindingand
MsmqIntegrationBindingon modern .NET. Sending is wired end-to-endvia the
MSMQ.MessagingNuGet (community port ofSystem.Messaging);listeners and any other server-side concern remain out of scope and
should live in CoreWCF.
Stats
(2 transactional scenarios env-var-gated by design — see below)
-warnaserrorPublic surface (23 types)
System.ServiceModel:NetMsmqBinding,MsmqBindingBase,NetMsmqSecurity,NetMsmqSecurityMode,MsmqTransportSecurity,MessageSecurityOverMsmq,MsmqAuthenticationMode,MsmqEncryptionAlgorithm,MsmqSecureHashAlgorithm,DeadLetterQueue,QueueTransferProtocol,MsmqException,PoisonMessageException,MsmqPoisonMessageExceptionSystem.ServiceModel.Channels:MsmqBindingElementBase,MsmqTransportBindingElementSystem.ServiceModel.MsmqIntegration:MsmqIntegrationBinding,MsmqIntegrationBindingElement,MsmqIntegrationSecurity,MsmqIntegrationSecurityMode,MsmqMessageSerializationFormat,MsmqMessage<T>,MsmqIntegrationMessagePropertyKey architectural decisions (open for review)
MSMQ.Messaging1.0.4 as a hardPackageReferenceinstead ofre-implementing the native MSMQ P/Invoke layer. Saves ~1,300 LOC
from the reference source and matches netfx semantics one-for-one.
net10.0only (notnet462). Full .NET Framework already shipsthe same public types in
System.ServiceModel.dll; co-targetingproduces CS0436 ambiguity for every public type.
ref/project, noSystem.ServiceModel.Shimtype-forwards— matches the modern pattern of
NetNamedPipe,Federation,UnixDomainSocket.AddressAccessDeniedExceptionmappings →CommunicationExceptionbecause the type lives in
System.ServiceModel.NetNamedPipe(notin
Primitives) and shipping our own copy would conflict atconsume time. Could be cleaned up by promoting it to
Primitives.MessageQueueTransactionType.Automaticfor ambient transactions —delegates to mqrt.dll's native DTC, no custom
IEnlistmentNotificationneeded.Bugs caught + fixed during the port
MessageQueueException.ErrorCodeis the generic HRESULT(0x80004005), not the native MQ_ERROR_* code. Had to convert through
MessageQueueErrorCodeforMsmqException.Normalizedto map.Regression:
MsmqMessagingInteropTest.cannot be flipped reliably from inside the xunit-console host. The
two transactional scenarios are env-var-gated
(
WCF_MSMQ_ENABLE_DTC_TESTS=true); product correctness is verifiedout-of-process. Regression:
MsmqConditionsTest.CI
azure-pipelines-arcade-PR.ymlneeds no edits — Helix alreadydiscovers
Scenarios/**/*.IntegrationTests.csproj.eng/SendToHelix.projgets a one-PropertyGroupHelixPreCommandsto best-effort
Enable-WindowsOptionalFeature MSMQ-Server+Start-Service MSMQon Windows workers.[Condition(MsmqInstalled)]skips the scenarios.Follow-ups (intentionally deferred)
IOutputSessionChannelso the channelinter-ops with netfx WCF services hosted with
SessionMode.Required.Today we send one MSMQ message per
Send()with auuid:{Guid}session id.
MsmqUri.ActiveDirectory+DLQtranslators — not on thehappy path; non-breaking add.
— replace the scenario tests' "send + read-back-via-MSMQ.Messaging"
pattern with a real WCF client → CoreWCF host → WCF client
round-trip.
Open questions for reviewers
MSMQ.Messagingas a runtime dependency, or prefer aP/Invoke reimplementation?
AddressAccessDeniedExceptionfromNetNamedPipetoPrimitivesso we can use the right mapping?Enable-WindowsOptionalFeature MSMQ-Servertowindows.11.amd64.client.openHelix image directly, or keep theper-workitem enable in
SendToHelix.proj?