Skip to content

Add System.ServiceModel.Msmq — client-side MSMQ transport - #5958

Open
afifi-ins wants to merge 3 commits into
dotnet:mainfrom
afifi-ins:feature/msmq-porting
Open

Add System.ServiceModel.Msmq — client-side MSMQ transport#5958
afifi-ins wants to merge 3 commits into
dotnet:mainfrom
afifi-ins:feature/msmq-porting

Conversation

@afifi-ins

Copy link
Copy Markdown
Contributor

Adds a new client transport package, System.ServiceModel.Msmq, that
provides the WCF client-side surface of .NET Framework's NetMsmqBinding
and MsmqIntegrationBinding on modern .NET. Sending is wired end-to-end
via the MSMQ.Messaging NuGet (community port of System.Messaging);
listeners and any other server-side concern remain out of scope and
should live in CoreWCF.

Stats

  • 15 commits · 71 files · +5,536 LOC
  • 23 public types ported (full netfx client surface)
  • 150/150 unit tests pass
  • 3/5 scenario tests pass against real localhost MSMQ
    (2 transactional scenarios env-var-gated by design — see below)
  • 0 warnings, 0 errors under -warnaserror

Public surface (23 types)

  • System.ServiceModel: NetMsmqBinding, MsmqBindingBase,
    NetMsmqSecurity, NetMsmqSecurityMode, MsmqTransportSecurity,
    MessageSecurityOverMsmq, MsmqAuthenticationMode,
    MsmqEncryptionAlgorithm, MsmqSecureHashAlgorithm,
    DeadLetterQueue, QueueTransferProtocol, MsmqException,
    PoisonMessageException, MsmqPoisonMessageException
  • System.ServiceModel.Channels: MsmqBindingElementBase,
    MsmqTransportBindingElement
  • System.ServiceModel.MsmqIntegration: MsmqIntegrationBinding,
    MsmqIntegrationBindingElement, MsmqIntegrationSecurity,
    MsmqIntegrationSecurityMode, MsmqMessageSerializationFormat,
    MsmqMessage<T>, MsmqIntegrationMessageProperty

Key architectural decisions (open for review)

  1. MSMQ.Messaging 1.0.4 as a hard PackageReference instead of
    re-implementing the native MSMQ P/Invoke layer. Saves ~1,300 LOC
    from the reference source and matches netfx semantics one-for-one.
  2. net10.0 only (not net462). Full .NET Framework already ships
    the same public types in System.ServiceModel.dll; co-targeting
    produces CS0436 ambiguity for every public type.
  3. No ref/ project, no System.ServiceModel.Shim type-forwards
    — matches the modern pattern of NetNamedPipe, Federation,
    UnixDomainSocket.
  4. AddressAccessDeniedException mappings → CommunicationException
    because the type lives in System.ServiceModel.NetNamedPipe (not
    in Primitives) and shipping our own copy would conflict at
    consume time. Could be cleaned up by promoting it to Primitives.
  5. MessageQueueTransactionType.Automatic for ambient transactions —
    delegates to mqrt.dll's native DTC, no custom
    IEnlistmentNotification needed.
  6. Receive-side / server hosting out of scope — use CoreWCF.

Bugs caught + fixed during the port

  1. MessageQueueException.ErrorCode is the generic HRESULT
    (0x80004005), not the native MQ_ERROR_* code. Had to convert through
    MessageQueueErrorCode for MsmqException.Normalized to map.
    Regression: MsmqMessagingInteropTest.
  2. .NET 8+ disables implicit DTC promotion by default and the property
    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 verified
    out-of-process. Regression: MsmqConditionsTest.

CI

  • azure-pipelines-arcade-PR.yml needs no edits — Helix already
    discovers Scenarios/**/*.IntegrationTests.csproj.
  • eng/SendToHelix.proj gets a one-PropertyGroup HelixPreCommands
    to best-effort Enable-WindowsOptionalFeature MSMQ-Server +
    Start-Service MSMQ on Windows workers.
  • On non-admin workers the enable silently no-ops and
    [Condition(MsmqInstalled)] skips the scenarios.

Follow-ups (intentionally deferred)

  1. Session-gram framing for IOutputSessionChannel so the channel
    inter-ops with netfx WCF services hosted with SessionMode.Required.
    Today we send one MSMQ message per Send() with a uuid:{Guid}
    session id.
  2. MsmqUri.ActiveDirectory + DLQ translators — not on the
    happy path; non-breaking add.
  3. CoreWCF MSMQ host once CoreWCF ships an MSMQ transport package
    — 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

  1. Approve MSMQ.Messaging as a runtime dependency, or prefer a
    P/Invoke reimplementation?
  2. Promote AddressAccessDeniedException from NetNamedPipe to
    Primitives so we can use the right mapping?
  3. Is session-gram framing a v1 blocker, or v1.1?
  4. Add Enable-WindowsOptionalFeature MSMQ-Server to
    windows.11.amd64.client.open Helix image directly, or keep the
    per-workitem enable in SendToHelix.proj?

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
afifi-ins force-pushed the feature/msmq-porting branch from 5a373f1 to 64371d9 Compare July 24, 2026 04:14
afifi-ins and others added 2 commits August 3, 2026 08:29
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant