diff --git a/System.ServiceModel.sln b/System.ServiceModel.sln index 16c21a1eee1..ace0041531b 100644 --- a/System.ServiceModel.sln +++ b/System.ServiceModel.sln @@ -83,6 +83,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ServiceModel.NetFram EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ServiceModel.NetNamedPipe", "src\System.ServiceModel.NetNamedPipe\src\System.ServiceModel.NetNamedPipe.csproj", "{5ECB8887-D7EE-449F-9439-35D0BBBB1E07}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ServiceModel.Msmq", "src\System.ServiceModel.Msmq\src\System.ServiceModel.Msmq.csproj", "{697D1BB3-BBF5-40CE-BA60-EA5945D783BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ServiceModel.Msmq.Tests", "src\System.ServiceModel.Msmq\tests\System.ServiceModel.Msmq.Tests.csproj", "{44F85C39-7BD4-489F-A4FC-D387DF925F40}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Binding.Msmq.IntegrationTests", "src\System.Private.ServiceModel\tests\Scenarios\Binding\Msmq\Binding.Msmq.IntegrationTests.csproj", "{B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.ServiceModel.UnixDomainSocket", "src\System.ServiceModel.UnixDomainSocket\src\System.ServiceModel.UnixDomainSocket.csproj", "{1664DB18-8451-43C0-8A85-2DD9189C3897}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Binding.UDS.IntegrationTests", "src\System.Private.ServiceModel\tests\Scenarios\Binding\UDS\Binding.UDS.IntegrationTests.csproj", "{B7C7D4F1-DE4D-421B-9CE9-C7320A503D58}" @@ -579,6 +585,42 @@ Global {5ECB8887-D7EE-449F-9439-35D0BBBB1E07}.Release|x64.Build.0 = Release|Any CPU {5ECB8887-D7EE-449F-9439-35D0BBBB1E07}.Release|x86.ActiveCfg = Release|Any CPU {5ECB8887-D7EE-449F-9439-35D0BBBB1E07}.Release|x86.Build.0 = Release|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Debug|x64.Build.0 = Debug|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Debug|x86.Build.0 = Debug|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Release|Any CPU.Build.0 = Release|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Release|x64.ActiveCfg = Release|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Release|x64.Build.0 = Release|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Release|x86.ActiveCfg = Release|Any CPU + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB}.Release|x86.Build.0 = Release|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Debug|x64.ActiveCfg = Debug|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Debug|x64.Build.0 = Debug|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Debug|x86.ActiveCfg = Debug|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Debug|x86.Build.0 = Debug|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Release|Any CPU.Build.0 = Release|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Release|x64.ActiveCfg = Release|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Release|x64.Build.0 = Release|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Release|x86.ActiveCfg = Release|Any CPU + {44F85C39-7BD4-489F-A4FC-D387DF925F40}.Release|x86.Build.0 = Release|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Debug|x64.Build.0 = Debug|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Debug|x86.Build.0 = Debug|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Release|Any CPU.Build.0 = Release|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Release|x64.ActiveCfg = Release|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Release|x64.Build.0 = Release|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Release|x86.ActiveCfg = Release|Any CPU + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38}.Release|x86.Build.0 = Release|Any CPU {1664DB18-8451-43C0-8A85-2DD9189C3897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1664DB18-8451-43C0-8A85-2DD9189C3897}.Debug|Any CPU.Build.0 = Debug|Any CPU {1664DB18-8451-43C0-8A85-2DD9189C3897}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -760,6 +802,9 @@ Global {88918456-A2B2-431F-BB95-BAAD2818BFC7} = {DFDC71CF-6E65-481D-99D7-C35ED7EF6D4E} {B7C7D4F1-DE4D-421B-9CE9-C7320A503D58} = {D6302510-AB10-4775-BCE9-98FA96FDEB76} {FD188537-21AF-48C5-84FF-F8758F4C42A8} = {DFDC71CF-6E65-481D-99D7-C35ED7EF6D4E} + {697D1BB3-BBF5-40CE-BA60-EA5945D783BB} = {D6302510-AB10-4775-BCE9-98FA96FDEB76} + {44F85C39-7BD4-489F-A4FC-D387DF925F40} = {DFDC71CF-6E65-481D-99D7-C35ED7EF6D4E} + {B7AC3382-C6DE-4BD2-910E-70BA53E7DA38} = {D6302510-AB10-4775-BCE9-98FA96FDEB76} {0CB62138-1E7D-4D9D-B7BF-4FCB79BC8EAA} = {D6302510-AB10-4775-BCE9-98FA96FDEB76} {7D7DACFC-D1DA-48C0-9BA1-C3A414780658} = {D6302510-AB10-4775-BCE9-98FA96FDEB76} EndGlobalSection diff --git a/eng/SendToHelix.proj b/eng/SendToHelix.proj index 00a4945dd02..87f997ad269 100644 --- a/eng/SendToHelix.proj +++ b/eng/SendToHelix.proj @@ -98,6 +98,16 @@ $(HelixPreCommands);security default-keychain -d user -s $HOME/Library/Keychains/login.keychain-db + + + $(HelixPreCommands);powershell -NoProfile -ExecutionPolicy Bypass -Command "try { Enable-WindowsOptionalFeature -Online -FeatureName MSMQ-Server -All -NoRestart -ErrorAction SilentlyContinue | Out-Null } catch {}; try { Start-Service MSMQ -ErrorAction SilentlyContinue } catch {}" + + $(HelixPreCommands);set PATH=%HELIX_CORRELATION_PAYLOAD%\dotnet-cli%3B%PATH% diff --git a/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalTestDetectors.cs b/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalTestDetectors.cs index 82b63a682b7..eb55160c726 100644 --- a/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalTestDetectors.cs +++ b/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalTestDetectors.cs @@ -253,5 +253,42 @@ internal static string GetSPN() return TestProperties.GetProperty(TestProperties.NegotiateTestSpn_PropertyName); } + // Detector used by [ConditionalFact(nameof(MsmqInstalled))]. + // Returns true if the MSMQ feature is installed on the host (we + // check for mqrt.dll under System32). MSMQ scenario tests need + // a running queue manager; this gate keeps them skipped on hosts + // that don't have the Windows MSMQ feature enabled. + public static bool IsMsmqInstalled() + { + if (!IsWindows()) + { + return false; + } + string system32 = Environment.GetFolderPath(Environment.SpecialFolder.System); + return !string.IsNullOrEmpty(system32) + && System.IO.File.Exists(System.IO.Path.Combine(system32, "mqrt.dll")); + } + + // Detector used by [ConditionalFact(nameof(ImplicitDtcEnabled))]. + // .NET 8+ disables implicit DTC promotion by default. The + // AppContext switch has to be set via the consuming process's + // `runtimeconfig.json`: + // "System.Transactions.EnableImplicitDistributedTransactions": "true" + // Setting the corresponding TransactionManager property from + // application code is not reliable from inside an xunit test + // host because the DTC proxy reads the value before our setter + // runs. We therefore gate tests on the env var + // WCF_MSMQ_ENABLE_DTC_TESTS=true + // which the dev / CI explicitly sets when the runtime property + // is known to be in place. + public static bool IsImplicitDtcEnabled() + { + if (!IsWindows()) + { + return false; + } + string env = Environment.GetEnvironmentVariable("WCF_MSMQ_ENABLE_DTC_TESTS"); + return string.Equals(env, "true", StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalWcfTest.cs b/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalWcfTest.cs index f5f1f035095..b406980bd2b 100644 --- a/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalWcfTest.cs +++ b/src/System.Private.ServiceModel/tests/Common/Infrastructure/ConditionalWcfTest.cs @@ -103,6 +103,25 @@ public static bool IsNotWindows() return !Is_Windows(); } + // Returns 'true' if the MSMQ Windows feature is installed on the + // client machine. Use as a [Condition] for any MSMQ scenario test + // that needs an actual queue manager. + public static bool MsmqInstalled() + { + return GetConditionValue(nameof(MsmqInstalled), + ConditionalTestDetectors.IsMsmqInstalled); + } + + // Returns 'true' if TransactionManager.ImplicitDistributedTransactions + // is currently set. MSMQ scenarios that need a TransactionScope use + // this gate so they skip cleanly on hosts where implicit DTC isn't + // enabled (the .NET 8+ default) instead of failing. + public static bool ImplicitDtcEnabled() + { + return GetConditionValue(nameof(ImplicitDtcEnabled), + ConditionalTestDetectors.IsImplicitDtcEnabled); + } + // Returns 'true' if both the server and the client are domain-joined. public static bool Domain_Joined() { diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/Binding.Msmq.IntegrationTests.csproj b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/Binding.Msmq.IntegrationTests.csproj new file mode 100644 index 00000000000..19c2a3839c1 --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/Binding.Msmq.IntegrationTests.csproj @@ -0,0 +1,16 @@ + + + $(ScenarioTestTargetFrameworks) + false + true + false + + + + + + + + + + diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/NetMsmqClientScenarioTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/NetMsmqClientScenarioTests.cs new file mode 100644 index 00000000000..ed27d6882c1 --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/NetMsmqClientScenarioTests.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.IO; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using Infrastructure.Common; +using MSMQ.Messaging; +using Xunit; + +using WcfMessage = System.ServiceModel.Channels.Message; +using MsmqRawMessage = MSMQ.Messaging.Message; + +namespace Binding.Msmq.IntegrationTests +{ + // End-to-end scenario tests for NetMsmqBinding's send path. Each + // test provisions a fresh private queue, sends through our client + // channel, then verifies the message arrived by reading the queue + // back with MSMQ.Messaging directly. No service host is required; + // we exercise the wire path against the local queue manager. + [SupportedOSPlatform("windows")] + public class NetMsmqClientScenarioTests : ConditionalWcfTest, IDisposable + { + private readonly string _shortName; + private readonly string _machineQueuePath; + private readonly Uri _queueUri; + + public NetMsmqClientScenarioTests() + { + _shortName = "wcf-test-" + Guid.NewGuid().ToString("N"); + _machineQueuePath = ".\\private$\\" + _shortName; + _queueUri = new Uri("net.msmq://localhost/private/" + _shortName); + } + + public void Dispose() + { + try + { + if (MessageQueue.Exists(_machineQueuePath)) + { + MessageQueue.Delete(_machineQueuePath); + } + } + catch { /* swallow on cleanup */ } + } + + [WcfFact] + [Condition(nameof(MsmqInstalled))] + public void Send_NonTransactional_MessageArrivesInQueue() + { + MessageQueue.Create(_machineQueuePath, transactional: false); + + SendOne("hello"); + + using var receiver = new MessageQueue("FormatName:DIRECT=OS:" + _machineQueuePath); + using MsmqRawMessage received = receiver.Receive(TimeSpan.FromSeconds(10)); + Assert.NotNull(received); + Assert.NotNull(received.BodyStream); + Assert.True(received.BodyStream.Length > 0); + } + + [WcfFact] + [Condition(nameof(MsmqInstalled))] + public void Send_ToNonExistentQueue_ThrowsEndpointNotFoundException() + { + var binding = new NetMsmqBinding(NetMsmqSecurityMode.None) + { + Durable = false, + ExactlyOnce = false, + }; + IChannelFactory factory = binding.BuildChannelFactory(); + try + { + factory.Open(); + Uri missing = new Uri("net.msmq://localhost/private/does-not-exist-" + Guid.NewGuid().ToString("N")); + IOutputChannel channel = factory.CreateChannel(new EndpointAddress(missing)); + channel.Open(); + try + { + WcfMessage msg = WcfMessage.CreateMessage(MessageVersion.Soap12WSAddressing10, "urn:test/echo", "x"); + EndpointNotFoundException ex = Assert.Throws(() => channel.Send(msg)); + Assert.IsType(ex.InnerException); + } + finally + { + channel.Abort(); + } + } + finally + { + factory.Close(); + } + } + + [WcfFact] + [Condition(nameof(MsmqInstalled))] + public void Send_NonTransactional_BodyBytesRoundTripBinaryEncoder() + { + MessageQueue.Create(_machineQueuePath, transactional: false); + + byte[] sentEncoded = SendOne("payload-42", returnEncodedBody: true); + + using var receiver = new MessageQueue("FormatName:DIRECT=OS:" + _machineQueuePath); + using MsmqRawMessage received = receiver.Receive(TimeSpan.FromSeconds(10)); + using var ms = new MemoryStream(); + received.BodyStream.CopyTo(ms); + Assert.Equal(sentEncoded, ms.ToArray()); + } + + private byte[] SendOne(string bodyText, bool returnEncodedBody = false) + { + var binding = new NetMsmqBinding(NetMsmqSecurityMode.None) + { + Durable = true, + ExactlyOnce = false, + }; + IChannelFactory factory = binding.BuildChannelFactory(); + try + { + factory.Open(); + IOutputChannel channel = factory.CreateChannel(new EndpointAddress(_queueUri)); + channel.Open(); + try + { + WcfMessage msg = WcfMessage.CreateMessage(MessageVersion.Soap12WSAddressing10, "urn:test/echo", bodyText); + byte[] encoded = null; + if (returnEncodedBody) + { + using MessageBuffer buffer = msg.CreateBufferedCopy(int.MaxValue); + WcfMessage previewMsg = buffer.CreateMessage(); + MessageEncoder encoder = new BinaryMessageEncodingBindingElement().CreateMessageEncoderFactory().Encoder; + var preview = encoder.WriteMessage(previewMsg, int.MaxValue, BufferManager.CreateBufferManager(64 * 1024, 64 * 1024)); + encoded = new byte[preview.Count]; + Buffer.BlockCopy(preview.Array, preview.Offset, encoded, 0, preview.Count); + msg = buffer.CreateMessage(); + } + channel.Send(msg); + return encoded; + } + finally + { + channel.Close(); + } + } + finally + { + factory.Close(); + } + } + } +} diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/NetMsmqTransactionalScenarioTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/NetMsmqTransactionalScenarioTests.cs new file mode 100644 index 00000000000..0a27c310f89 --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Msmq/NetMsmqTransactionalScenarioTests.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Transactions; +using Infrastructure.Common; +using MSMQ.Messaging; +using Xunit; + +namespace Binding.Msmq.IntegrationTests +{ + // Verifies that the MSMQ output channel properly participates in an + // ambient System.Transactions.Transaction: commit means the message + // is on the queue; rollback means it is not. Both tests target a + // transactional MSMQ queue (the only kind that honors transactional + // sends). + [SupportedOSPlatform("windows")] + public class NetMsmqTransactionalScenarioTests : ConditionalWcfTest, IDisposable + { + private readonly string _shortName; + private readonly string _machineQueuePath; + private readonly Uri _queueUri; + + public NetMsmqTransactionalScenarioTests() + { + _shortName = "wcf-tx-test-" + Guid.NewGuid().ToString("N"); + _machineQueuePath = ".\\private$\\" + _shortName; + _queueUri = new Uri("net.msmq://localhost/private/" + _shortName); + } + + public void Dispose() + { + try + { + if (MessageQueue.Exists(_machineQueuePath)) + { + MessageQueue.Delete(_machineQueuePath); + } + } + catch { /* swallow on cleanup */ } + } + + [WcfFact] + [Condition(nameof(MsmqInstalled))] + [Condition(nameof(ImplicitDtcEnabled))] + public void Send_InCommittedTransactionScope_MessageArrivesInQueue() + { + MessageQueue.Create(_machineQueuePath, transactional: true); + + using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew)) + { + SendOne("committed"); + scope.Complete(); + } + + using var receiver = new MessageQueue("FormatName:DIRECT=OS:" + _machineQueuePath); + using MSMQ.Messaging.Message received = receiver.Receive(MessageQueueTransactionType.Single); + Assert.NotNull(received); + Assert.True(received.BodyStream.Length > 0); + } + + [WcfFact] + [Condition(nameof(MsmqInstalled))] + [Condition(nameof(ImplicitDtcEnabled))] + public void Send_InRolledBackTransactionScope_MessageDoesNotArrive() + { + MessageQueue.Create(_machineQueuePath, transactional: true); + + using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew)) + { + SendOne("rolled-back"); + // scope.Complete() not called -> Dispose rolls back. + } + + using var receiver = new MessageQueue("FormatName:DIRECT=OS:" + _machineQueuePath); + // Expect no message; use a short timeout so the test fails fast on regression. + MSMQ.Messaging.MessageQueueException ex = Assert.Throws(() => + receiver.Receive(TimeSpan.FromSeconds(2), MessageQueueTransactionType.Single)); + Assert.Equal(MessageQueueErrorCode.IOTimeout, ex.MessageQueueErrorCode); + } + + // Note on transactional scenario tests: the .NET 8+ runtime + // disables implicit DTC promotion by default and the property + // must be flipped before any code in the process touches + // System.Transactions — which is not reliable from inside an + // xunit test host because the discovery layer initialises DTC + // first. These two tests are therefore gated behind + // [Condition(ImplicitDtcEnabled)], which keys off the + // WCF_MSMQ_ENABLE_DTC_TESTS=true env var. Treat them as a + // manual / dev-machine check unless and until we get a + // dedicated host process. Product correctness has been verified + // out-of-process via a standalone harness that sets + // TransactionManager.ImplicitDistributedTransactions=true as + // its very first statement. + + private void SendOne(string bodyText) + { + var binding = new NetMsmqBinding(NetMsmqSecurityMode.None) + { + Durable = true, + ExactlyOnce = true, + }; + IChannelFactory factory = binding.BuildChannelFactory(); + try + { + factory.Open(); + IOutputChannel channel = factory.CreateChannel(new EndpointAddress(_queueUri)); + channel.Open(); + try + { + var msg = System.ServiceModel.Channels.Message.CreateMessage( + MessageVersion.Soap12WSAddressing10, "urn:test/echo", bodyText); + channel.Send(msg); + } + finally + { + channel.Close(); + } + } + finally + { + factory.Close(); + } + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/CompatibilitySuppressions.xml b/src/System.ServiceModel.Msmq/src/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..5697160f01d --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/CompatibilitySuppressions.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/System.ServiceModel.Msmq/src/README.md b/src/System.ServiceModel.Msmq/src/README.md new file mode 100644 index 00000000000..330452de4e0 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/README.md @@ -0,0 +1,73 @@ +## About + +`System.ServiceModel.Msmq` provides the types that permit SOAP messages to be exchanged using MSMQ (Microsoft Message Queuing) as the transport. The package implements the client-side surface of the .NET Framework `NetMsmqBinding` and `MsmqIntegrationBinding`. + +This package is **Windows-only**. The Windows MSMQ feature (`MSMQ-Server` or the client subset) must be installed on the machine running the client. + +## Runtime dependency + +The package is self-contained on the managed side: it depends only on `System.ServiceModel.Primitives`. The send path P/Invokes directly into `mqrt.dll` from the Windows MSMQ feature — no extra NuGet packages required. + +## Installing + +``` +dotnet add package System.ServiceModel.Msmq +``` + +## Usage + +Send a SOAP message over `NetMsmqBinding` to a local private queue: + +```csharp +using System.ServiceModel; +using System.ServiceModel.Channels; + +var binding = new NetMsmqBinding(NetMsmqSecurityMode.None) +{ + Durable = true, + ExactlyOnce = false, +}; + +IChannelFactory factory = binding.BuildChannelFactory(); +factory.Open(); + +IOutputChannel channel = factory.CreateChannel( + new EndpointAddress("net.msmq://localhost/private/orders")); +channel.Open(); + +Message message = Message.CreateMessage( + MessageVersion.Soap12WSAddressing10, + "urn:contoso/orders/submit", + new { OrderId = 42, Sku = "ABC" }); +channel.Send(message); + +channel.Close(); +factory.Close(); +``` + +Integrate with a legacy MSMQ application via `MsmqIntegrationBinding` and `MsmqMessage`: + +```csharp +using System.ServiceModel.MsmqIntegration; + +var msg = new MsmqMessage(myPayload) +{ + Label = "order-42", + Priority = MessagePriority.High, + CorrelationId = "11111111-2222-3333-4444-555555555555\\1", +}; +// channel.Send(...) carrying msg as the WCF message body. +``` + +## Differences from the .NET Framework + +This client-only port deliberately omits server-side concerns. See the [`dotnet/wcf` README](https://github.com/dotnet/wcf/) for the broader WCF client roadmap. Highlights: + +- Server hosting (channel listeners, poison-message handlers, queue receive loops) is not in scope — use [CoreWCF](https://github.com/CoreWCF/CoreWCF) for service hosting. +- `System.Configuration`-based XML binding configuration is not supported; configure bindings in code. +- `IOutputSessionChannel` is supported but emits one MSMQ message per `Send` rather than the .NET Framework "session gram" framing — wire-level interop with netfx `SessionMode.Required` services is tracked as a follow-up. + +## Feedback & Contributing + +To explore the project or contribute, visit our [GitHub repository](https://github.com/dotnet/wcf/). +For reporting issues or providing feedback, please [open an issue on GitHub](https://github.com/dotnet/wcf). diff --git a/src/System.ServiceModel.Msmq/src/Resources/Strings.resx b/src/System.ServiceModel.Msmq/src/Resources/Strings.resx new file mode 100644 index 00000000000..5c5facd9e93 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/Strings.resx @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + MSMQ is not supported on this platform. + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + The value of this argument must be non-negative. + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + The specified channel type {0} is not supported by this channel manager. + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + The scheme of the URI is not valid for this MSMQ address translator. + + + The MSMQ queue URI is missing the host segment. + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.cs.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.cs.xlf new file mode 100644 index 00000000000..2cd0220f675 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.cs.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.de.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.de.xlf new file mode 100644 index 00000000000..aebe7fe6812 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.de.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.es.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.es.xlf new file mode 100644 index 00000000000..8c696d39265 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.es.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.fr.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.fr.xlf new file mode 100644 index 00000000000..1434b68f9dc --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.fr.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.it.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.it.xlf new file mode 100644 index 00000000000..c9a081c6d6c --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.it.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ja.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ja.xlf new file mode 100644 index 00000000000..3068156f3f3 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ja.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ko.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ko.xlf new file mode 100644 index 00000000000..d3fa37123f2 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ko.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.pl.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.pl.xlf new file mode 100644 index 00000000000..65e174ed05f --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.pl.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.pt-BR.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.pt-BR.xlf new file mode 100644 index 00000000000..d7162a0fa5b --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.pt-BR.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ru.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ru.xlf new file mode 100644 index 00000000000..b3133e14085 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.ru.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.tr.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.tr.xlf new file mode 100644 index 00000000000..61b609e5839 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.tr.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.zh-Hans.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.zh-Hans.xlf new file mode 100644 index 00000000000..7a022d6f205 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.zh-Hans.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.zh-Hant.xlf b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.zh-Hant.xlf new file mode 100644 index 00000000000..455b0a0e60a --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/Resources/xlf/Strings.zh-Hant.xlf @@ -0,0 +1,92 @@ + + + + + + The specified channel type {0} is not supported by this channel manager. + The specified channel type {0} is not supported by this channel manager. + + + + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + The MSMQ message id is not in the expected 'GUID\COUNTER' format. + + + + The scheme of the URI is not valid for this MSMQ address translator. + The scheme of the URI is not valid for this MSMQ address translator. + + + + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + The MSMQ client package does not support building channel listeners. Use CoreWCF to host an MSMQ service. + + + + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + The native MSMQ message buffer has already been finalized and cannot accept additional properties. + + + + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + The MSMQ.Messaging assembly could not be loaded. Add a PackageReference to the MSMQ.Messaging NuGet package to your application so System.ServiceModel.Msmq can dispatch messages to the underlying queue. + + + + The value of this argument must be non-negative. + The value of this argument must be non-negative. + + + + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + Failed to open the MSMQ queue at format name '{0}'. See InnerException for the native MQ_ERROR_* code. + + + + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + The transport channel detected an incoming message that cannot be processed and that exceeded the configured allowed retries. Refer to the inner exception for details of the failure, and the documentation for the MsmqPoisonMessageException class for further guidance. + + + + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + The MSMQ send operation failed with native error 0x{0}. See InnerException for the underlying MQ_ERROR_* code. + + + + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + The MSMQ send channel is not yet implemented in this build of System.ServiceModel.Msmq. + + + + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + A port may not be specified on a net.msmq queue URI when using the native MSMQ transport protocol. + + + + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + Private queues are addressed using '/private/<name>' in net.msmq URIs. The legacy '/private$' syntax is not supported. + + + + The MSMQ queue URI is missing the host segment. + The MSMQ queue URI is missing the host segment. + + + + MSMQ is not supported on this platform. + MSMQ is not supported on this platform. + + + + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + Timeout must be greater than or equal to TimeSpan.Zero. To disable timeout, specify TimeSpan.MaxValue. + + + + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + Timeouts larger than Int32.MaxValue TotalMilliseconds (approximately 24 days) cannot be honored. To disable timeout, specify TimeSpan.MaxValue. + + + + + \ No newline at end of file diff --git a/src/System.ServiceModel.Msmq/src/System.ServiceModel.Msmq.csproj b/src/System.ServiceModel.Msmq/src/System.ServiceModel.Msmq.csproj new file mode 100644 index 00000000000..8347c780113 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System.ServiceModel.Msmq.csproj @@ -0,0 +1,31 @@ + + + $(WcfAssemblyVersion) + Microsoft + System.ServiceModel.Msmq + true + true + $(Ship_WcfPackages) + $(DotNetVersion) + System.ServiceModel + true + + false + + + + + + + + + + + true + FxResources.$(AssemblyName).SR + false + System.SR + + + diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/DtcTransactionBridge.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/DtcTransactionBridge.cs new file mode 100644 index 00000000000..fd2c6f052ef --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/DtcTransactionBridge.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Transactions; + +namespace System.ServiceModel.Channels +{ + // Bridges a System.Transactions.Transaction into the + // ITransaction COM interface that mqrt!MQSendMessage expects. + // + // TransactionInterop.GetDtcTransaction returns an IDtcTransaction + // whose IID is exactly the OLE ITransaction IID, so the COM + // pointer we marshal here can be passed straight through to + // MQSendMessage without an extra QueryInterface. + // + // The COM ref-counting contract is: + // * GetComInterfaceForObject returns a pointer with AddRef. + // * MQSendMessage either holds onto it for the duration of the + // two-phase commit or copies whatever it needs and returns. + // * We Release in a finally, balancing the AddRef. + // + // On .NET 8+, the runtime no longer promotes a local transaction + // to a distributed one implicitly; callers wanting to flow + // Transaction.Current through MSMQ must opt in via + // "System.Transactions.EnableImplicitDistributedTransactions": "true" + // in their runtimeconfig.json. Without it, GetDtcTransaction itself + // throws NotSupportedException long before we get to the COM + // marshaling. + [SupportedOSPlatform("windows")] + internal static class DtcTransactionBridge + { + internal static IntPtr AcquireITransactionPointer(Transaction transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + IDtcTransaction dtcTransaction = TransactionInterop.GetDtcTransaction(transaction); + return Marshal.GetComInterfaceForObject(dtcTransaction, typeof(IDtcTransaction)); + } + + internal static void ReleaseITransactionPointer(IntPtr ptr) + { + if (ptr != IntPtr.Zero) + { + Marshal.Release(ptr); + } + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqBindingElementBase.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqBindingElementBase.cs new file mode 100644 index 00000000000..7c1f5e4fdad --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqBindingElementBase.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.Channels +{ + public abstract class MsmqBindingElementBase : TransportBindingElement + { + private Uri _customDeadLetterQueue; + private DeadLetterQueue _deadLetterQueue; + private bool _durable; + private bool _exactlyOnce; + private int _maxRetryCycles; + private ReceiveErrorHandling _receiveErrorHandling; + private int _receiveRetryCount; + private TimeSpan _retryCycleDelay; + private TimeSpan _timeToLive; + private MsmqTransportSecurity _msmqTransportSecurity; + private bool _useMsmqTracing; + private bool _useSourceJournal; + private bool _receiveContextEnabled; + + internal MsmqBindingElementBase() + { + _customDeadLetterQueue = null; + _deadLetterQueue = MsmqDefaults.DeadLetterQueue; + _durable = MsmqDefaults.Durable; + _exactlyOnce = MsmqDefaults.ExactlyOnce; + _maxRetryCycles = MsmqDefaults.MaxRetryCycles; + _receiveContextEnabled = MsmqDefaults.ReceiveContextEnabled; + _receiveErrorHandling = MsmqDefaults.ReceiveErrorHandling; + _receiveRetryCount = MsmqDefaults.ReceiveRetryCount; + _retryCycleDelay = MsmqDefaults.RetryCycleDelay; + _timeToLive = MsmqDefaults.TimeToLive; + _msmqTransportSecurity = new MsmqTransportSecurity(); + _useMsmqTracing = MsmqDefaults.UseMsmqTracing; + _useSourceJournal = MsmqDefaults.UseSourceJournal; + } + + internal MsmqBindingElementBase(MsmqBindingElementBase elementToBeCloned) : base(elementToBeCloned) + { + _customDeadLetterQueue = elementToBeCloned._customDeadLetterQueue; + _deadLetterQueue = elementToBeCloned._deadLetterQueue; + _durable = elementToBeCloned._durable; + _exactlyOnce = elementToBeCloned._exactlyOnce; + _maxRetryCycles = elementToBeCloned._maxRetryCycles; + _msmqTransportSecurity = new MsmqTransportSecurity(elementToBeCloned.MsmqTransportSecurity); + _receiveContextEnabled = elementToBeCloned._receiveContextEnabled; + _receiveErrorHandling = elementToBeCloned._receiveErrorHandling; + _receiveRetryCount = elementToBeCloned._receiveRetryCount; + _retryCycleDelay = elementToBeCloned._retryCycleDelay; + _timeToLive = elementToBeCloned._timeToLive; + _useMsmqTracing = elementToBeCloned._useMsmqTracing; + _useSourceJournal = elementToBeCloned._useSourceJournal; + } + + internal abstract MsmqUri.IAddressTranslator AddressTranslator { get; } + + public Uri CustomDeadLetterQueue + { + get { return _customDeadLetterQueue; } + set { _customDeadLetterQueue = value; } + } + + public DeadLetterQueue DeadLetterQueue + { + get { return _deadLetterQueue; } + set + { + if (!DeadLetterQueueHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _deadLetterQueue = value; + } + } + + public bool Durable + { + get { return _durable; } + set { _durable = value; } + } + + public bool TransactedReceiveEnabled + { + get { return _exactlyOnce; } + } + + public bool ExactlyOnce + { + get { return _exactlyOnce; } + set { _exactlyOnce = value; } + } + + public int ReceiveRetryCount + { + get { return _receiveRetryCount; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, SR.MsmqNonNegativeArgumentExpected); + } + _receiveRetryCount = value; + } + } + + public int MaxRetryCycles + { + get { return _maxRetryCycles; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, SR.MsmqNonNegativeArgumentExpected); + } + _maxRetryCycles = value; + } + } + + public MsmqTransportSecurity MsmqTransportSecurity + { + get { return _msmqTransportSecurity; } + internal set { _msmqTransportSecurity = value; } + } + + public bool ReceiveContextEnabled + { + get { return _receiveContextEnabled; } + set { _receiveContextEnabled = value; } + } + + public ReceiveErrorHandling ReceiveErrorHandling + { + get { return _receiveErrorHandling; } + set + { + if (!IsReceiveErrorHandlingDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _receiveErrorHandling = value; + } + } + + public TimeSpan RetryCycleDelay + { + get { return _retryCycleDelay; } + set + { + ValidateTimeoutValue(value); + _retryCycleDelay = value; + } + } + + public TimeSpan TimeToLive + { + get { return _timeToLive; } + set + { + ValidateTimeoutValue(value); + _timeToLive = value; + } + } + + public bool UseMsmqTracing + { + get { return _useMsmqTracing; } + set { _useMsmqTracing = value; } + } + + public bool UseSourceJournal + { + get { return _useSourceJournal; } + set { _useSourceJournal = value; } + } + + public override T GetProperty(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (typeof(T) == typeof(ISecurityCapabilities)) + { + return null; + } + if (typeof(T) == typeof(IBindingDeliveryCapabilities)) + { + return (T)(object)new BindingDeliveryCapabilitiesHelper(); + } + return base.GetProperty(context); + } + + private static bool IsReceiveErrorHandlingDefined(ReceiveErrorHandling value) + { + return value == ReceiveErrorHandling.Fault + || value == ReceiveErrorHandling.Drop + || value == ReceiveErrorHandling.Reject + || value == ReceiveErrorHandling.Move; + } + + private static void ValidateTimeoutValue(TimeSpan value) + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), value, SR.SFxTimeoutOutOfRange0); + } + if (value.TotalMilliseconds > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(value), value, SR.SFxTimeoutOutOfRangeTooBig); + } + } + + private sealed class BindingDeliveryCapabilitiesHelper : IBindingDeliveryCapabilities + { + bool IBindingDeliveryCapabilities.AssuresOrderedDelivery => false; + bool IBindingDeliveryCapabilities.QueuedDelivery => true; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqDefaults.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqDefaults.cs new file mode 100644 index 00000000000..c9d327144bb --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqDefaults.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Net.Security; +using System.ServiceModel.MsmqIntegration; +using System.ServiceModel.Security; + +namespace System.ServiceModel.Channels +{ + internal static class MsmqDefaults + { + internal const MessageCredentialType DefaultClientCredentialType = MessageCredentialType.Windows; + internal const DeadLetterQueue DeadLetterQueue = System.ServiceModel.DeadLetterQueue.System; + internal const bool Durable = true; + internal const bool ExactlyOnce = true; + internal const bool ReceiveContextEnabled = true; + internal const int MaxRetryCycles = 2; + internal const int MaxPoolSize = 8; + internal const MsmqAuthenticationMode MsmqAuthenticationMode = System.ServiceModel.MsmqAuthenticationMode.WindowsDomain; + internal const MsmqEncryptionAlgorithm MsmqEncryptionAlgorithm = System.ServiceModel.MsmqEncryptionAlgorithm.RC4Stream; + internal const MsmqSecureHashAlgorithm DefaultMsmqSecureHashAlgorithm = System.ServiceModel.MsmqSecureHashAlgorithm.Sha256; + internal static MsmqSecureHashAlgorithm MsmqSecureHashAlgorithm => DefaultMsmqSecureHashAlgorithm; + internal const ProtectionLevel MsmqProtectionLevel = ProtectionLevel.Sign; + internal const ReceiveErrorHandling ReceiveErrorHandling = System.ServiceModel.ReceiveErrorHandling.Fault; + internal const int ReceiveRetryCount = 5; + internal const QueueTransferProtocol QueueTransferProtocol = System.ServiceModel.QueueTransferProtocol.Native; + internal const MsmqMessageSerializationFormat MsmqMessageSerializationFormat = MsmqIntegration.MsmqMessageSerializationFormat.Xml; + internal const string RetryCycleDelayString = "00:30:00"; + internal static TimeSpan RetryCycleDelay => TimeSpan.Parse(RetryCycleDelayString); + internal const string TimeToLiveString = "1.00:00:00"; + internal static TimeSpan TimeToLive => TimeSpan.Parse(TimeToLiveString); + internal const bool UseActiveDirectory = false; + internal const bool UseSourceJournal = false; + internal const bool UseMsmqTracing = false; + internal const string ValidityDurationString = "00:05:00"; + internal static TimeSpan ValidityDuration => TimeSpan.Parse(ValidityDurationString); + internal static SecurityAlgorithmSuite MessageSecurityAlgorithmSuite => SecurityAlgorithmSuite.Default; + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqMessagingInterop.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqMessagingInterop.cs new file mode 100644 index 00000000000..7331511b388 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqMessagingInterop.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.ServiceModel.MsmqIntegration; +using System.Transactions; + +namespace System.ServiceModel.Channels +{ + // Send-path facade. Walks an outgoing payload + optional + // MsmqIntegrationMessageProperty bag into a NativeMsmqMessage and + // dispatches it through MsmqQueue.Send. All MSMQ-specific marshaling + // and DTC enlistment live in the dedicated native layer + // (UnsafeNativeMethods / NativeMsmqMessage / MsmqQueue / + // DtcTransactionBridge). This file is intentionally small — it owns + // only the "fold caller intent into native primitives" decisions. + [SupportedOSPlatform("windows")] + internal static class MsmqMessagingInterop + { + internal static void Send( + string formatName, + byte[] body, + int offset, + int count, + bool exactlyOnce, + TimeSpan timeToLive, + TimeSpan sendTimeout) + { + using var message = new NativeMsmqMessage(); + PopulateBaseProperties(message, body, offset, count, timeToLive); + SendCore(formatName, message, exactlyOnce, sendTimeout); + } + + internal static void Send( + string formatName, + byte[] body, + int offset, + int count, + MsmqIntegrationMessageProperty property, + bool exactlyOnce, + TimeSpan timeToLive, + TimeSpan sendTimeout) + { + using var message = new NativeMsmqMessage(); + PopulateBaseProperties(message, body, offset, count, timeToLive); + property?.ApplyTo(message); + SendCore(formatName, message, exactlyOnce, sendTimeout); + } + + private static void PopulateBaseProperties(NativeMsmqMessage message, byte[] body, int offset, int count, TimeSpan timeToLive) + { + message.SetBody(body, offset, count); + if (timeToLive != default) + { + message.SetTimeToBeReceived(timeToLive); + } + } + + // Picks the MSMQ transaction mode that matches the binding's + // ExactlyOnce contract and the ambient System.Transactions + // transaction: + // + // ExactlyOnce Transaction.Current MSMQ mode + // ----------- ------------------- ----------- + // true non-null Automatic (enlist in ambient DTC tx) + // true null Single (start a one-shot MSMQ tx) + // false any None (non-transactional send) + // + // MsmqTransactionMode.Automatic delegates enlistment to mqrt.dll + // which uses the native MSMQ DTC integration — the MSMQ send + // commits or aborts atomically with any other resource managers + // participating in Transaction.Current. + internal static MsmqTransactionMode GetTransactionMode(bool exactlyOnce, Transaction ambient) + { + if (!exactlyOnce) + { + return MsmqTransactionMode.None; + } + return ambient != null + ? MsmqTransactionMode.Automatic + : MsmqTransactionMode.Single; + } + + private static void SendCore( + string formatName, + NativeMsmqMessage message, + bool exactlyOnce, + TimeSpan sendTimeout) + { + _ = sendTimeout; // mqrt!MQSendMessage has no per-call timeout. + using MsmqQueue queue = MsmqQueue.OpenForSend(formatName); + MsmqTransactionMode mode = GetTransactionMode(exactlyOnce, Transaction.Current); + queue.Send(message, mode); + } + } +} + diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputChannel.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputChannel.cs new file mode 100644 index 00000000000..216ab07fc1f --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputChannel.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.IO; +using System.Runtime.Versioning; +using System.Threading.Tasks; + +namespace System.ServiceModel.Channels +{ + // Send-only IOutputChannel implementation. Serializes the WCF Message + // through the binding's MessageEncoder and dispatches the bytes to MSMQ + // via the MSMQ.Messaging.MessageQueue high-level API. Transactional + // sends are flowed through System.Transactions.Transaction.Current when + // the channel is built from an ExactlyOnce binding. + [SupportedOSPlatform("windows")] + internal sealed class MsmqOutputChannel : ChannelBase, IOutputChannel + { + private readonly MsmqOutputChannelFactory _factory; + private readonly EndpointAddress _remoteAddress; + private readonly Uri _via; + private readonly string _formatName; + + internal MsmqOutputChannel(MsmqOutputChannelFactory factory, EndpointAddress remoteAddress, Uri via) + : base(factory) + { + _factory = factory; + _remoteAddress = remoteAddress; + _via = via; + _formatName = MsmqUri.UriToFormatNameByScheme(via); + } + + public EndpointAddress RemoteAddress => _remoteAddress; + + public Uri Via => _via; + + public void Send(Message message) + { + Send(message, DefaultSendTimeout); + } + + public void Send(Message message, TimeSpan timeout) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + ThrowIfDisposedOrNotOpen(); + + ArraySegment encoded = _factory.MessageEncoder.WriteMessage( + message, int.MaxValue, _factory.BufferManager); + try + { + SendToQueue(encoded, timeout); + } + finally + { + _factory.BufferManager.ReturnBuffer(encoded.Array); + } + } + + public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state) + => BeginSend(message, DefaultSendTimeout, callback, state); + + public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state) + => Task.Run(() => Send(message, timeout)).ToApm(callback, state); + + public void EndSend(IAsyncResult result) => result.ToApmEnd(); + + private void SendToQueue(ArraySegment encoded, TimeSpan timeout) + { + // MSMQ.Messaging is loaded reflectively so the rest of the package + // can be exercised by unit tests on non-Windows hosts that lack + // the dependency. On Windows, the call delegates to + // MSMQ.Messaging.MessageQueue.Send(message[, transaction]). + MsmqMessagingInterop.Send( + _formatName, + encoded.Array, + encoded.Offset, + encoded.Count, + _factory.BindingElement.ExactlyOnce, + _factory.BindingElement.TimeToLive, + timeout); + } + + protected override void OnAbort() { } + + protected override void OnClose(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndClose(IAsyncResult result) => result.ToApmEnd(); + + protected override void OnOpen(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndOpen(IAsyncResult result) => result.ToApmEnd(); + + protected override Task OnOpenAsync(TimeSpan timeout) => Task.CompletedTask; + + protected override Task OnCloseAsync(TimeSpan timeout) => Task.CompletedTask; + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputChannelFactory.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputChannelFactory.cs new file mode 100644 index 00000000000..6a893066f99 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputChannelFactory.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace System.ServiceModel.Channels +{ + // Send-side channel factory backed by MSMQ.Messaging.MessageQueue. + // This is intentionally minimal: it supports IOutputChannel for + // NetMsmqBinding (binary-encoded SOAP messages going into a single + // queue). IOutputSessionChannel, transactional retries, message + // pooling, and the receive side land in later slices. + [SupportedOSPlatform("windows")] + internal sealed class MsmqOutputChannelFactory : ChannelFactoryBase + { + private readonly MsmqTransportBindingElement _bindingElement; + private readonly MessageEncoderFactory _messageEncoderFactory; + private readonly BufferManager _bufferManager; + + internal MsmqOutputChannelFactory(MsmqTransportBindingElement bindingElement, BindingContext context) + : base(context.Binding) + { + _bindingElement = bindingElement ?? throw new ArgumentNullException(nameof(bindingElement)); + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + MessageEncodingBindingElement encodingElement = + context.BindingParameters.Find() + ?? new BinaryMessageEncodingBindingElement(); + _messageEncoderFactory = encodingElement.CreateMessageEncoderFactory(); + _bufferManager = BufferManager.CreateBufferManager( + bindingElement.MaxBufferPoolSize, + (int)Math.Min(bindingElement.MaxReceivedMessageSize, int.MaxValue)); + } + + internal MsmqTransportBindingElement BindingElement => _bindingElement; + internal MessageEncoder MessageEncoder => _messageEncoderFactory.Encoder; + internal BufferManager BufferManager => _bufferManager; + + public override T GetProperty() + { + if (typeof(T) == typeof(MessageVersion)) + { + return (T)(object)MessageEncoder.MessageVersion; + } + return base.GetProperty(); + } + + protected override IOutputChannel OnCreateChannel(EndpointAddress address, Uri via) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + return new MsmqOutputChannel(this, address, via ?? address.Uri); + } + + protected override void OnAbort() + { + _bufferManager?.Clear(); + } + + protected override void OnClose(TimeSpan timeout) + { + _bufferManager?.Clear(); + } + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + { + return Task.CompletedTask.ToApm(callback, state); + } + + protected override void OnEndClose(IAsyncResult result) + { + _bufferManager?.Clear(); + result.ToApmEnd(); + } + + protected override void OnOpen(TimeSpan timeout) + { + } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + { + return Task.CompletedTask.ToApm(callback, state); + } + + protected override void OnEndOpen(IAsyncResult result) + { + result.ToApmEnd(); + } + + protected override Task OnOpenAsync(TimeSpan timeout) => Task.CompletedTask; + + protected override Task OnCloseAsync(TimeSpan timeout) + { + _bufferManager?.Clear(); + return Task.CompletedTask; + } + } + + internal static class TaskApmExtensions + { + internal static IAsyncResult ToApm(this Task task, AsyncCallback callback, object state) + { + var tcs = new TaskCompletionSource(state); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(true); + } + callback?.Invoke(tcs.Task); + }, TaskScheduler.Default); + return tcs.Task; + } + + internal static void ToApmEnd(this IAsyncResult result) + { + ((Task)result).GetAwaiter().GetResult(); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputSessionChannel.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputSessionChannel.cs new file mode 100644 index 00000000000..28a926dc3a6 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputSessionChannel.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.Threading.Tasks; + +namespace System.ServiceModel.Channels +{ + // Send-only IOutputSessionChannel. + // + // NOTE: this implementation sends one MSMQ message per Send() call, + // exposing a per-channel Guid session id via IOutputSession. It does + // NOT yet produce the .NET Framework "session-gram" wire format (a + // single concatenated MSMQ message carrying the session preamble + + // every buffered application message, emitted on Close). That format + // is required for full interoperability with netfx WCF endpoints + // hosted with SessionMode.Required. Tracked as a follow-up; see + // plan.md, slice 4b decisions. + [SupportedOSPlatform("windows")] + internal sealed class MsmqOutputSessionChannel : ChannelBase, IOutputSessionChannel + { + private readonly MsmqOutputSessionChannelFactory _factory; + private readonly EndpointAddress _remoteAddress; + private readonly Uri _via; + private readonly string _formatName; + private readonly OutputSession _session; + + internal MsmqOutputSessionChannel(MsmqOutputSessionChannelFactory factory, EndpointAddress remoteAddress, Uri via) + : base(factory) + { + _factory = factory; + _remoteAddress = remoteAddress; + _via = via; + _formatName = MsmqUri.UriToFormatNameByScheme(via); + _session = new OutputSession(); + } + + public EndpointAddress RemoteAddress => _remoteAddress; + + public Uri Via => _via; + + public IOutputSession Session => _session; + + public void Send(Message message) => Send(message, DefaultSendTimeout); + + public void Send(Message message, TimeSpan timeout) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + ThrowIfDisposedOrNotOpen(); + + ArraySegment encoded = _factory.MessageEncoder.WriteMessage( + message, int.MaxValue, _factory.BufferManager); + try + { + MsmqMessagingInterop.Send( + _formatName, + encoded.Array, + encoded.Offset, + encoded.Count, + _factory.BindingElement.ExactlyOnce, + _factory.BindingElement.TimeToLive, + timeout); + } + finally + { + _factory.BufferManager.ReturnBuffer(encoded.Array); + } + } + + public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state) + => BeginSend(message, DefaultSendTimeout, callback, state); + + public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state) + => Task.Run(() => Send(message, timeout)).ToApm(callback, state); + + public void EndSend(IAsyncResult result) => result.ToApmEnd(); + + protected override void OnAbort() { } + + protected override void OnClose(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndClose(IAsyncResult result) => result.ToApmEnd(); + + protected override void OnOpen(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndOpen(IAsyncResult result) => result.ToApmEnd(); + + protected override Task OnOpenAsync(TimeSpan timeout) => Task.CompletedTask; + + protected override Task OnCloseAsync(TimeSpan timeout) => Task.CompletedTask; + + private sealed class OutputSession : IOutputSession + { + private readonly string _id = "uuid:" + Guid.NewGuid().ToString("D"); + public string Id => _id; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputSessionChannelFactory.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputSessionChannelFactory.cs new file mode 100644 index 00000000000..fe112efd221 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqOutputSessionChannelFactory.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.Threading.Tasks; + +namespace System.ServiceModel.Channels +{ + [SupportedOSPlatform("windows")] + internal sealed class MsmqOutputSessionChannelFactory : ChannelFactoryBase + { + private readonly MsmqTransportBindingElement _bindingElement; + private readonly MessageEncoderFactory _messageEncoderFactory; + private readonly BufferManager _bufferManager; + + internal MsmqOutputSessionChannelFactory(MsmqTransportBindingElement bindingElement, BindingContext context) + : base(context.Binding) + { + _bindingElement = bindingElement ?? throw new ArgumentNullException(nameof(bindingElement)); + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + MessageEncodingBindingElement encodingElement = + context.BindingParameters.Find() + ?? new BinaryMessageEncodingBindingElement(); + _messageEncoderFactory = encodingElement.CreateMessageEncoderFactory(); + _bufferManager = BufferManager.CreateBufferManager( + bindingElement.MaxBufferPoolSize, + (int)Math.Min(bindingElement.MaxReceivedMessageSize, int.MaxValue)); + } + + internal MsmqTransportBindingElement BindingElement => _bindingElement; + internal MessageEncoder MessageEncoder => _messageEncoderFactory.Encoder; + internal BufferManager BufferManager => _bufferManager; + + public override T GetProperty() + { + if (typeof(T) == typeof(MessageVersion)) + { + return (T)(object)MessageEncoder.MessageVersion; + } + return base.GetProperty(); + } + + protected override IOutputSessionChannel OnCreateChannel(EndpointAddress address, Uri via) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + return new MsmqOutputSessionChannel(this, address, via ?? address.Uri); + } + + protected override void OnAbort() => _bufferManager?.Clear(); + + protected override void OnClose(TimeSpan timeout) => _bufferManager?.Clear(); + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndClose(IAsyncResult result) + { + _bufferManager?.Clear(); + result.ToApmEnd(); + } + + protected override void OnOpen(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndOpen(IAsyncResult result) => result.ToApmEnd(); + + protected override Task OnOpenAsync(TimeSpan timeout) => Task.CompletedTask; + + protected override Task OnCloseAsync(TimeSpan timeout) + { + _bufferManager?.Clear(); + return Task.CompletedTask; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqQueue.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqQueue.cs new file mode 100644 index 00000000000..c14668083b1 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqQueue.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.Transactions; + +namespace System.ServiceModel.Channels +{ + // Send-side facade over the native MSMQ queue. Owns the + // SafeMsmqQueueHandle for its lifetime and serializes calls + // through MQSendMessage with the appropriate transaction handle. + // + // Stateless w.r.t. property layout — the caller passes a fully + // populated NativeMsmqMessage, and MsmqQueue is responsible only + // for opening the queue, calling Send, mapping the HRESULT, and + // managing DTC enlistment. + [SupportedOSPlatform("windows")] + internal sealed class MsmqQueue : IDisposable + { + private readonly SafeMsmqQueueHandle _handle; + private bool _disposed; + + private MsmqQueue(SafeMsmqQueueHandle handle) + { + _handle = handle; + } + + internal static MsmqQueue OpenForSend(string formatName) + { + SafeMsmqQueueHandle handle = SafeMsmqQueueHandle.OpenForSend(formatName); + return new MsmqQueue(handle); + } + + // Sends a single message. txMode selects between non-transactional, + // MSMQ-single-message, and DTC-flowed. + internal void Send(NativeMsmqMessage message, MsmqTransactionMode txMode) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + if (_disposed) + { + throw new ObjectDisposedException(nameof(MsmqQueue)); + } + IntPtr propsPtr = message.Freeze(); + + int hr; + switch (txMode) + { + case MsmqTransactionMode.None: + hr = UnsafeNativeMethods.MQSendMessage(_handle, propsPtr, UnsafeNativeMethods.MQ_NO_TRANSACTION); + break; + case MsmqTransactionMode.Single: + hr = UnsafeNativeMethods.MQSendMessage(_handle, propsPtr, UnsafeNativeMethods.MQ_SINGLE_MESSAGE); + break; + case MsmqTransactionMode.Automatic: + IntPtr dtcPtr = DtcTransactionBridge.AcquireITransactionPointer(Transaction.Current); + try + { + hr = UnsafeNativeMethods.MQSendMessage(_handle, propsPtr, dtcPtr); + } + finally + { + DtcTransactionBridge.ReleaseITransactionPointer(dtcPtr); + } + break; + default: + throw new ArgumentOutOfRangeException(nameof(txMode)); + } + + // FAILED(hr): the high bit being set indicates a true + // failure. Warnings such as MQ_INFORMATION_PROPERTY + // (0x400E0001) mean the call still completed and the + // message was enqueued — they should not surface as + // exceptions. This matches COM's SUCCEEDED/FAILED contract + // and the .NET Framework MSMQ reference source. + if (hr < 0) + { + throw new MsmqException(SR.Format(SR.MsmqSendFailed, hr.ToString("x8")), hr).Normalized; + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + _handle?.Dispose(); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqTransactionMode.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqTransactionMode.cs new file mode 100644 index 00000000000..b3e557f2ea2 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqTransactionMode.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.Channels +{ + // Selects the pTransaction argument passed to mqrt!MQSendMessage. + // Determined by GetTransactionMode in MsmqMessagingInterop based + // on the binding's ExactlyOnce flag and the ambient + // System.Transactions.Transaction.Current. + internal enum MsmqTransactionMode + { + // Non-transactional send (MQ_NO_TRANSACTION / NULL). + None = 0, + + // Single-message MSMQ transaction (MQ_SINGLE_MESSAGE / (ITransaction*)3). + Single = 1, + + // DTC-flowed transaction — we marshal Transaction.Current as + // an IDtcTransaction COM pointer. + Automatic = 2, + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqTransportBindingElement.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqTransportBindingElement.cs new file mode 100644 index 00000000000..315c0772d33 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqTransportBindingElement.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; + +namespace System.ServiceModel.Channels +{ + public sealed class MsmqTransportBindingElement : MsmqBindingElementBase + { + private int _maxPoolSize = MsmqDefaults.MaxPoolSize; + private bool _useActiveDirectory = MsmqDefaults.UseActiveDirectory; + private QueueTransferProtocol _queueTransferProtocol = MsmqDefaults.QueueTransferProtocol; + + public MsmqTransportBindingElement() + { + } + + private MsmqTransportBindingElement(MsmqTransportBindingElement elementToBeCloned) + : base(elementToBeCloned) + { + _useActiveDirectory = elementToBeCloned._useActiveDirectory; + _maxPoolSize = elementToBeCloned._maxPoolSize; + _queueTransferProtocol = elementToBeCloned._queueTransferProtocol; + } + + internal override MsmqUri.IAddressTranslator AddressTranslator + { + // Filled in by the slice that ports MsmqUri's per-protocol translators. + get { return null; } + } + + public int MaxPoolSize + { + get { return _maxPoolSize; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, SR.MsmqNonNegativeArgumentExpected); + } + _maxPoolSize = value; + } + } + + public QueueTransferProtocol QueueTransferProtocol + { + get { return _queueTransferProtocol; } + set + { + if (!QueueTransferProtocolHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _queueTransferProtocol = value; + } + } + + public override string Scheme => "net.msmq"; + + public bool UseActiveDirectory + { + get { return _useActiveDirectory; } + set { _useActiveDirectory = value; } + } + + public override BindingElement Clone() + { + return new MsmqTransportBindingElement(this); + } + + public override bool CanBuildChannelFactory(BindingContext context) + { + return typeof(TChannel) == typeof(IOutputChannel) + || typeof(TChannel) == typeof(IOutputSessionChannel); + } + + [SupportedOSPlatform("windows")] + public override IChannelFactory BuildChannelFactory(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (typeof(TChannel) == typeof(IOutputChannel)) + { + return (IChannelFactory)(object)new MsmqOutputChannelFactory(this, context); + } + if (typeof(TChannel) == typeof(IOutputSessionChannel)) + { + return (IChannelFactory)(object)new MsmqOutputSessionChannelFactory(this, context); + } + throw new ArgumentException(SR.Format(SR.ChannelTypeNotSupported, typeof(TChannel)), "TChannel"); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqUri.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqUri.cs new file mode 100644 index 00000000000..526235bbe13 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/MsmqUri.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Globalization; +using System.Net; +using System.Text; + +namespace System.ServiceModel.Channels +{ + // Address translation for net.msmq:// and msmq.formatname: URIs. + // Active Directory (path-name lookup) and the dead-letter-queue resolver + // from the .NET Framework reference source are intentionally omitted — + // both rely on infrastructure (MsmqFormatName, DnsCache) that is not + // yet ported. They can be added later without breaking changes. + internal static class MsmqUri + { + private static IAddressTranslator s_netMsmq; + private static IAddressTranslator s_srmp; + private static IAddressTranslator s_srmps; + private static IAddressTranslator s_formatName; + + internal static IAddressTranslator NetMsmqAddressTranslator + => s_netMsmq ??= new NetMsmqTranslator(); + + internal static IAddressTranslator SrmpAddressTranslator + => s_srmp ??= new SrmpTranslator(); + + internal static IAddressTranslator SrmpsAddressTranslator + => s_srmps ??= new SrmpSecureTranslator(); + + internal static IAddressTranslator FormatNameAddressTranslator + => s_formatName ??= new FormatNameTranslator(); + + internal static string UriToFormatNameByScheme(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + if (uri.Scheme == NetMsmqAddressTranslator.Scheme) + { + return NetMsmqAddressTranslator.UriToFormatName(uri); + } + if (uri.Scheme == FormatNameAddressTranslator.Scheme) + { + return FormatNameAddressTranslator.UriToFormatName(uri); + } + throw new ArgumentException(SR.Format(SR.MsmqInvalidScheme), nameof(uri)); + } + + internal interface IAddressTranslator + { + string Scheme { get; } + string UriToFormatName(Uri uri); + } + + private static void AppendQueueName(StringBuilder builder, string relativePath, string separator) + { + const string privatePart = "/private"; + + if (relativePath.StartsWith("/private$", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(SR.MsmqWrongPrivateQueueSyntax); + } + + if (relativePath.StartsWith(privatePart, StringComparison.OrdinalIgnoreCase)) + { + if (privatePart.Length == relativePath.Length) + { + builder.Append("private$").Append(separator); + relativePath = "/"; + } + else if (relativePath[privatePart.Length] == '/') + { + builder.Append("private$").Append(separator); + relativePath = relativePath.Substring(privatePart.Length); + } + } + builder.Append(relativePath.Substring(1)); + } + + private static void ValidateNetMsmqUri(Uri uri, string expectedScheme, bool allowPort) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + if (uri.Scheme != expectedScheme) + { + throw new ArgumentException(SR.MsmqInvalidScheme, nameof(uri)); + } + if (string.IsNullOrEmpty(uri.Host)) + { + throw new ArgumentException(SR.MsmqWrongUri, nameof(uri)); + } + if (!allowPort && uri.Port != -1) + { + throw new ArgumentException(SR.MsmqUnexpectedPort, nameof(uri)); + } + } + + private sealed class NetMsmqTranslator : IAddressTranslator + { + public string Scheme => "net.msmq"; + + public string UriToFormatName(Uri uri) + { + ValidateNetMsmqUri(uri, Scheme, allowPort: false); + + var builder = new StringBuilder("DIRECT="); + if (string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase)) + { + builder.Append("OS:."); + } + else + { + builder.Append(IPAddress.TryParse(uri.Host, out _) ? "TCP:" : "OS:"); + builder.Append(uri.Host); + } + builder.Append('\\'); + AppendQueueName(builder, Uri.UnescapeDataString(uri.PathAndQuery), "\\"); + return builder.ToString(); + } + } + + private abstract class SrmpTranslatorBase : IAddressTranslator + { + public string Scheme => "net.msmq"; + protected abstract string DirectScheme { get; } + + public string UriToFormatName(Uri uri) + { + ValidateNetMsmqUri(uri, Scheme, allowPort: true); + + var builder = new StringBuilder("DIRECT="); + builder.Append(DirectScheme); + builder.Append(uri.Host); + if (uri.Port != -1) + { + builder.Append(':').Append(uri.Port.ToString(CultureInfo.InvariantCulture)); + } + builder.Append("/msmq/"); + AppendQueueName(builder, Uri.UnescapeDataString(uri.PathAndQuery), "/"); + return builder.ToString(); + } + } + + private sealed class SrmpTranslator : SrmpTranslatorBase + { + protected override string DirectScheme => "http://"; + } + + private sealed class SrmpSecureTranslator : SrmpTranslatorBase + { + protected override string DirectScheme => "https://"; + } + + private sealed class FormatNameTranslator : IAddressTranslator + { + public string Scheme => "msmq.formatname"; + + public string UriToFormatName(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + if (uri.Scheme != Scheme) + { + throw new ArgumentException(SR.MsmqInvalidScheme, nameof(uri)); + } + return Uri.UnescapeDataString(uri.AbsoluteUri.Substring(Scheme.Length + 1)); + } + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/NativeMsmqMessage.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/NativeMsmqMessage.cs new file mode 100644 index 00000000000..f7ecb5a6947 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/NativeMsmqMessage.cs @@ -0,0 +1,379 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace System.ServiceModel.Channels +{ + // Builder for an outgoing MSMQ message represented as the native + // MQMSGPROPS structure that MQSendMessage consumes. + // + // Each call to a public setter appends one slot to three parallel + // arrays (ids / property variants / status), all of which are + // allocated in unmanaged memory via Marshal.AllocHGlobal. Pinned + // buffers (body bytes, label string, etc.) are tracked separately + // and freed on Dispose so the native call sees stable addresses for + // the lifetime of the Send. + // + // Layout notes: + // * MQPROPVARIANT is treated as a 24-byte slot on every arch. + // The header is fixed (8 bytes: vt + 3 reserved words) and the + // union spans the remaining 16 bytes, which is the upper bound + // used by MSMQ's property surface (DECIMAL / CAUB / BLOB). + // * Vector members write { cElems @ 8, pElems @ VectorElementsOffset }. + // VectorElementsOffset is 12 on x86, 16 on x64, accounting for + // pointer-width alignment of the second field in CAUB. + // * Wide strings are stored UNICODE (CharSet=Unicode on the + // P/Invoke side), allocated via Marshal.StringToHGlobalUni, + // and accompanied by a separate LEN propid (in characters). + [SupportedOSPlatform("windows")] + internal sealed class NativeMsmqMessage : IDisposable + { + private const int PropVariantSize = 24; + private static int VectorElementsOffset => IntPtr.Size == 8 ? 16 : 12; + + // Per-slot state — populated as the caller adds properties. + private readonly List _ids = new List(16); + // Each entry has its 24 bytes pre-written into a temp byte[] + // and then BLOB-copied into the unmanaged propVars buffer at + // Freeze time. This keeps the layout code in one place. + private readonly List _slotBytes = new List(16); + private readonly List _pinnedHandles = new List(); + private readonly List _hglobals = new List(); + + // Allocated only on Freeze. + private IntPtr _propIdsBuffer; + private IntPtr _propVarsBuffer; + private IntPtr _statusBuffer; + private IntPtr _propsStructBuffer; + private bool _frozen; + private bool _disposed; + + // ----- Typed setters ----- + + public void SetByte(uint propId, byte value) + { + var slot = new byte[PropVariantSize]; + WriteUShort(slot, 0, UnsafeNativeMethods.VT_UI1); + slot[8] = value; + Append(propId, slot); + } + + public void SetUInt32(uint propId, uint value) + { + var slot = new byte[PropVariantSize]; + WriteUShort(slot, 0, UnsafeNativeMethods.VT_UI4); + WriteUInt32(slot, 8, value); + Append(propId, slot); + } + + // Adds a length-prefixed unicode string. lengthPropId, if + // non-zero, also appends a VT_UI4 with the string's *character* + // count (not byte count). The string is stored in unmanaged + // memory and freed in Dispose(). + public void SetWideString(uint propId, string value, uint lengthPropId) + { + if (value == null) + { + return; + } + + IntPtr stringPtr = Marshal.StringToHGlobalUni(value); + _hglobals.Add(stringPtr); + + var slot = new byte[PropVariantSize]; + WriteUShort(slot, 0, UnsafeNativeMethods.VT_LPWSTR); + WriteIntPtr(slot, 8, stringPtr); + Append(propId, slot); + + if (lengthPropId != 0) + { + // Character count INCLUDING terminating null, per MSMQ docs. + SetUInt32(lengthPropId, (uint)(value.Length + 1)); + } + } + + // Adds a byte-vector. lengthPropId, if non-zero, also appends a + // VT_UI4 with the byte count. + public void SetByteVector(uint propId, byte[] value, uint lengthPropId) + { + if (value == null) + { + return; + } + + // Pin so MSMQ sees a stable address. Released in Dispose. + GCHandle pin = GCHandle.Alloc(value, GCHandleType.Pinned); + _pinnedHandles.Add(pin); + + var slot = new byte[PropVariantSize]; + WriteUShort(slot, 0, (ushort)(UnsafeNativeMethods.VT_VECTOR | UnsafeNativeMethods.VT_UI1)); + WriteUInt32(slot, 8, (uint)value.Length); + WriteIntPtr(slot, VectorElementsOffset, pin.AddrOfPinnedObject()); + Append(propId, slot); + + if (lengthPropId != 0) + { + SetUInt32(lengthPropId, (uint)value.Length); + } + } + + // Body is special: PROPID_M_BODY holds the byte vector and + // PROPID_M_BODY_SIZE holds the *actual* byte count (which can + // be smaller than the buffer when MSMQ writes a received body + // back into a pre-sized buffer; on the send path these are + // equal). + public void SetBody(byte[] body, int offset, int count) + { + if (body == null) + { + throw new ArgumentNullException(nameof(body)); + } + byte[] slice; + if (offset == 0 && count == body.Length) + { + slice = body; + } + else + { + slice = new byte[count]; + Buffer.BlockCopy(body, offset, slice, 0, count); + } + + GCHandle pin = GCHandle.Alloc(slice, GCHandleType.Pinned); + _pinnedHandles.Add(pin); + + var bodySlot = new byte[PropVariantSize]; + WriteUShort(bodySlot, 0, (ushort)(UnsafeNativeMethods.VT_VECTOR | UnsafeNativeMethods.VT_UI1)); + WriteUInt32(bodySlot, 8, (uint)slice.Length); + WriteIntPtr(bodySlot, VectorElementsOffset, pin.AddrOfPinnedObject()); + Append(UnsafeNativeMethods.PROPID_M_BODY, bodySlot); + + SetUInt32(UnsafeNativeMethods.PROPID_M_BODY_SIZE, (uint)slice.Length); + } + + public void SetTimeToBeReceived(TimeSpan value) + { + uint seconds = ToMsmqSeconds(value); + SetUInt32(UnsafeNativeMethods.PROPID_M_TIME_TO_BE_RECEIVED, seconds); + } + + public void SetTimeToReachQueue(TimeSpan value) + { + uint seconds = ToMsmqSeconds(value); + SetUInt32(UnsafeNativeMethods.PROPID_M_TIME_TO_REACH_QUEUE, seconds); + } + + // MSMQ timer fields are uint seconds. TimeSpan.MaxValue maps to + // the "infinite" sentinel; anything else is clamped to the + // [0, uint.MaxValue-1] range so it round-trips cleanly. + private static uint ToMsmqSeconds(TimeSpan value) + { + if (value == TimeSpan.MaxValue) + { + return uint.MaxValue; + } + double s = value.TotalSeconds; + if (s <= 0) + { + return 0; + } + if (s >= uint.MaxValue) + { + return uint.MaxValue - 1; + } + return (uint)s; + } + + // ----- Finalization ----- + + // Lays out the unmanaged MQMSGPROPS the native API expects. + // Returns a pointer that remains valid until Dispose. + public IntPtr Freeze() + { + if (_frozen) + { + return _propsStructBuffer; + } + int n = _ids.Count; + _propIdsBuffer = Marshal.AllocHGlobal(sizeof(uint) * n); + _propVarsBuffer = Marshal.AllocHGlobal(PropVariantSize * n); + _statusBuffer = Marshal.AllocHGlobal(sizeof(int) * n); + + for (int i = 0; i < n; i++) + { + Marshal.WriteInt32(_propIdsBuffer, i * sizeof(uint), unchecked((int)_ids[i])); + Marshal.Copy(_slotBytes[i], 0, _propVarsBuffer + i * PropVariantSize, PropVariantSize); + Marshal.WriteInt32(_statusBuffer, i * sizeof(int), 0); + } + + // Layout of MQMSGPROPS: + // DWORD cProp; (4 bytes) + // <4 bytes padding on x64> + // MSGPROPID* aPropID; (pointer) + // MQPROPVARIANT* aPropVar; (pointer) + // HRESULT* aStatus; (pointer) + // Total: 8 + IntPtr.Size * 3 on x64, 4 + IntPtr.Size * 3 on x86. + int headerSize = IntPtr.Size == 8 ? 8 : 4; + int structSize = headerSize + IntPtr.Size * 3; + _propsStructBuffer = Marshal.AllocHGlobal(structSize); + Marshal.WriteInt32(_propsStructBuffer, 0, n); + // Zero the padding word on x64 so the upper 32 bits of cProp + // can't be read as garbage on a future ABI revision. + if (IntPtr.Size == 8) + { + Marshal.WriteInt32(_propsStructBuffer, 4, 0); + } + Marshal.WriteIntPtr(_propsStructBuffer, headerSize + IntPtr.Size * 0, _propIdsBuffer); + Marshal.WriteIntPtr(_propsStructBuffer, headerSize + IntPtr.Size * 1, _propVarsBuffer); + Marshal.WriteIntPtr(_propsStructBuffer, headerSize + IntPtr.Size * 2, _statusBuffer); + + _frozen = true; + return _propsStructBuffer; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + + if (_propsStructBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_propsStructBuffer); + _propsStructBuffer = IntPtr.Zero; + } + if (_statusBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_statusBuffer); + _statusBuffer = IntPtr.Zero; + } + if (_propVarsBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_propVarsBuffer); + _propVarsBuffer = IntPtr.Zero; + } + if (_propIdsBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_propIdsBuffer); + _propIdsBuffer = IntPtr.Zero; + } + foreach (IntPtr p in _hglobals) + { + if (p != IntPtr.Zero) + { + Marshal.FreeHGlobal(p); + } + } + _hglobals.Clear(); + foreach (GCHandle h in _pinnedHandles) + { + if (h.IsAllocated) + { + h.Free(); + } + } + _pinnedHandles.Clear(); + } + + // ----- Internals ----- + + private void Append(uint id, byte[] slot) + { + if (_frozen) + { + throw new InvalidOperationException(SR.MsmqMessageAlreadyFrozen); + } + _ids.Add(id); + _slotBytes.Add(slot); + } + + private static void WriteUShort(byte[] buffer, int offset, ushort value) + { + buffer[offset] = (byte)(value & 0xFF); + buffer[offset + 1] = (byte)((value >> 8) & 0xFF); + } + + private static void WriteUInt32(byte[] buffer, int offset, uint value) + { + buffer[offset] = (byte)(value & 0xFF); + buffer[offset + 1] = (byte)((value >> 8) & 0xFF); + buffer[offset + 2] = (byte)((value >> 16) & 0xFF); + buffer[offset + 3] = (byte)((value >> 24) & 0xFF); + } + + private static void WriteIntPtr(byte[] buffer, int offset, IntPtr value) + { + if (IntPtr.Size == 8) + { + ulong v = unchecked((ulong)value.ToInt64()); + for (int i = 0; i < 8; i++) + { + buffer[offset + i] = (byte)(v & 0xFF); + v >>= 8; + } + } + else + { + uint v = unchecked((uint)value.ToInt32()); + for (int i = 0; i < 4; i++) + { + buffer[offset + i] = (byte)(v & 0xFF); + v >>= 8; + } + } + } + + // Exposes the calculated layout constants so unit tests can + // verify the marshaling code without invoking MSMQ. + internal static int PropVariantSizeForTests => PropVariantSize; + internal static int VectorElementsOffsetForTests => VectorElementsOffset; + + // Test helper: dump the freshly-built propvar slot bytes for + // a single property as a 24-byte array. Used by the + // NativeMsmqMessage marshaling unit tests. + internal byte[] GetSlotForTests(int index) => _slotBytes[index]; + internal uint GetPropIdForTests(int index) => _ids[index]; + internal int SlotCountForTests => _ids.Count; + + // Parses an MSMQ message-id string ("GUID\NUMBER") into the + // 20-byte binary form MSMQ expects for PROPID_M_CORRELATIONID: + // 16 bytes of GUID followed by a 4-byte little-endian counter. + // Throws FormatException on a string that doesn't match the + // expected shape — the caller is responsible for wrapping or + // ignoring. + internal static byte[] ParseMessageId(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new FormatException(SR.MsmqInvalidMessageId); + } + int sep = id.LastIndexOf('\\'); + if (sep < 0 || sep == 0 || sep == id.Length - 1) + { + throw new FormatException(SR.MsmqInvalidMessageId); + } + string guidPart = id.Substring(0, sep); + string idPart = id.Substring(sep + 1); + if (!Guid.TryParseExact(guidPart, "D", out Guid g)) + { + throw new FormatException(SR.MsmqInvalidMessageId); + } + if (!uint.TryParse(idPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint counter)) + { + throw new FormatException(SR.MsmqInvalidMessageId); + } + byte[] bytes = new byte[20]; + g.ToByteArray().CopyTo(bytes, 0); + BitConverter.GetBytes(counter).CopyTo(bytes, 16); + return bytes; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/SafeMsmqQueueHandle.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/SafeMsmqQueueHandle.cs new file mode 100644 index 00000000000..bb57547a66b --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/SafeMsmqQueueHandle.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace System.ServiceModel.Channels +{ + // SafeHandle wrapper around an MSMQ queue handle (QUEUEHANDLE). + // Ensures mqrt!MQCloseQueue is called even if the owning channel + // is finalized before close, and prevents the handle being passed + // to native code while a release is in flight. + [SupportedOSPlatform("windows")] + internal sealed class SafeMsmqQueueHandle : SafeHandle + { + private SafeMsmqQueueHandle() : base(IntPtr.Zero, ownsHandle: true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + // mqrt!MQCloseQueue returns MQ_OK on success. We swallow + // any failure here because the runtime calls us from the + // finalizer queue or from Dispose and has no recovery path. + return UnsafeNativeMethods.MQCloseQueue(handle) == UnsafeNativeMethods.S_OK; + } + + // Opens a queue for send access. Throws MsmqException.Normalized + // on failure, matching the rest of the send pipeline. + // + // `formatName` is the raw MSMQ format name as produced by the + // MsmqUri translator (e.g. "DIRECT=OS:.\\private$\\queue"). Some + // callers and string-handling layers in WCF prepend the + // "FormatName:" prefix that System.Messaging public APIs accept; + // mqrt!MQOpenQueue rejects that prefix as + // MQ_ERROR_ILLEGAL_FORMATNAME, so we strip it here as a + // convenience. + internal static SafeMsmqQueueHandle OpenForSend(string formatName) + { + if (formatName == null) + { + throw new ArgumentNullException(nameof(formatName)); + } + if (formatName.StartsWith("FormatName:", StringComparison.OrdinalIgnoreCase)) + { + formatName = formatName.Substring("FormatName:".Length); + } + + int hr = UnsafeNativeMethods.MQOpenQueue( + formatName, + UnsafeNativeMethods.MQ_SEND_ACCESS, + UnsafeNativeMethods.MQ_DENY_NONE, + out IntPtr rawHandle); + + // FAILED(hr): same COM contract as MsmqQueue.Send — only + // treat the high bit being set as a real failure. Warnings + // such as MQ_INFORMATION_FORMATNAME_BUFFER_TOO_SMALL + // (returned by MQOpenQueue in some edge cases) should not + // throw because the queue handle was still produced. + if (hr < 0) + { + throw new MsmqException(SR.Format(SR.MsmqOpenQueueFailed, formatName), hr).Normalized; + } + + var safe = new SafeMsmqQueueHandle(); + safe.SetHandle(rawHandle); + return safe; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/UnsafeNativeMethods.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/UnsafeNativeMethods.cs new file mode 100644 index 00000000000..fe2375a3133 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/Channels/UnsafeNativeMethods.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Transactions; + +namespace System.ServiceModel.Channels +{ + // Native MSMQ P/Invoke surface used by the send path. Only the + // subset of mqrt.dll required by the client-side IOutputChannel + // is declared here; the receive side lives in CoreWCF. + // + // Layout notes: + // * MQMSGPROPS is a struct of pointers — we marshal it manually + // through Marshal.AllocHGlobal'd buffers in NativeMsmqMessage, + // and the entry point takes IntPtr so we can pass it ref-free. + // * MQPROPVARIANT is a tagged union whose vector-member offsets + // depend on pointer size. We write each slot byte-by-byte via + // Marshal.Write* using NativeMsmqMessage.PropVariantSize and + // VectorElementsOffset, so the same code works on x86 and x64. + // * MQOpenQueue requires the queue name as a wide string; the + // CharSet is Unicode on every entry point. + // + // Transaction handle conventions, copied straight from mqmsg.h: + // MQ_NO_TRANSACTION = NULL (IntPtr.Zero) + // MQ_MTS_TRANSACTION = (ITransaction*)1 + // MQ_XA_TRANSACTION = (ITransaction*)2 + // MQ_SINGLE_MESSAGE = (ITransaction*)3 + // For DTC enlistment we pass an IDtcTransaction COM pointer (same + // IID as the OLE ITransaction interface) obtained from + // TransactionInterop.GetDtcTransaction. + [SupportedOSPlatform("windows")] + internal static class UnsafeNativeMethods + { + internal const string MqrtDll = "mqrt.dll"; + + // -- MQOpenQueue dwAccess flags -- + internal const int MQ_RECEIVE_ACCESS = 0x00000001; + internal const int MQ_SEND_ACCESS = 0x00000002; + internal const int MQ_PEEK_ACCESS = 0x00000020; + + // -- MQOpenQueue dwShareMode flags -- + internal const int MQ_DENY_NONE = 0x00000000; + internal const int MQ_DENY_RECEIVE_SHARE = 0x00000001; + + // -- VARTYPE values used by MSMQ properties -- + internal const ushort VT_NULL = 1; + internal const ushort VT_I2 = 2; + internal const ushort VT_I4 = 3; + internal const ushort VT_UI1 = 17; + internal const ushort VT_UI2 = 18; + internal const ushort VT_UI4 = 19; + internal const ushort VT_LPWSTR = 31; + internal const ushort VT_VECTOR = 0x1000; + + // -- PROPID_M_* property identifiers (subset used on the send path) -- + internal const uint PROPID_M_CLASS = 1; + internal const uint PROPID_M_MSGID = 2; + internal const uint PROPID_M_CORRELATIONID = 3; + internal const uint PROPID_M_PRIORITY = 4; + internal const uint PROPID_M_DELIVERY = 5; + internal const uint PROPID_M_ACKNOWLEDGE = 6; + internal const uint PROPID_M_JOURNAL = 7; + internal const uint PROPID_M_APPSPECIFIC = 8; + internal const uint PROPID_M_BODY = 9; + internal const uint PROPID_M_BODY_SIZE = 10; + internal const uint PROPID_M_LABEL = 11; + internal const uint PROPID_M_LABEL_LEN = 12; + internal const uint PROPID_M_TIME_TO_REACH_QUEUE = 13; + internal const uint PROPID_M_TIME_TO_BE_RECEIVED = 14; + internal const uint PROPID_M_RESP_QUEUE = 15; + internal const uint PROPID_M_RESP_QUEUE_LEN = 16; + internal const uint PROPID_M_ADMIN_QUEUE = 17; + internal const uint PROPID_M_ADMIN_QUEUE_LEN = 18; + internal const uint PROPID_M_EXTENSION = 24; + internal const uint PROPID_M_EXTENSION_LEN = 25; + internal const uint PROPID_M_BODY_TYPE = 36; + + // -- Transaction handle sentinels (passed in the pTransaction slot) -- + internal static readonly IntPtr MQ_NO_TRANSACTION = IntPtr.Zero; + internal static readonly IntPtr MQ_SINGLE_MESSAGE = new IntPtr(3); + + // S_OK + internal const int S_OK = 0; + + // mqrt.dll exports `MQOpenQueue` (unsuffixed) — it does *not* + // expose A/W variants. CharSet=Unicode + ExactSpelling=true on + // the plain symbol is the right binding for the wide-string + // overload that MSMQ provides. + // + // Note on the lpwcsFormatName argument: this is the raw MSMQ + // format name (e.g. "DIRECT=OS:.\\private$\\foo"), NOT the + // "FormatName:..." prefix used by System.Messaging public APIs + // such as MessageQueue.Path. SafeMsmqQueueHandle.OpenForSend + // is responsible for stripping that prefix if a caller hands it + // through. + [DllImport(MqrtDll, EntryPoint = "MQOpenQueue", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false)] + internal static extern int MQOpenQueue( + string lpwcsFormatName, + int dwAccess, + int dwShareMode, + out IntPtr phQueue); + + [DllImport(MqrtDll, EntryPoint = "MQCloseQueue", ExactSpelling = true, SetLastError = false)] + internal static extern int MQCloseQueue(IntPtr hQueue); + + // Non-transactional / Single / MQ_SINGLE_MESSAGE / explicit ITransaction* via IntPtr. + [DllImport(MqrtDll, EntryPoint = "MQSendMessage", ExactSpelling = true, SetLastError = false)] + internal static extern int MQSendMessage( + SafeMsmqQueueHandle hDestinationQueue, + IntPtr pMessageProps, + IntPtr pTransaction); + + // DTC enlistment overload — pTransaction is the IDtcTransaction + // COM pointer obtained from TransactionInterop. + [DllImport(MqrtDll, EntryPoint = "MQSendMessage", ExactSpelling = true, SetLastError = false)] + internal static extern int MQSendMessage( + SafeMsmqQueueHandle hDestinationQueue, + IntPtr pMessageProps, + [MarshalAs(UnmanagedType.Interface)] IDtcTransaction pTransaction); + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/DeadLetterQueue.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/DeadLetterQueue.cs new file mode 100644 index 00000000000..ad2c0846a7c --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/DeadLetterQueue.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel +{ + public enum DeadLetterQueue + { + None, + System, + Custom + } + + internal static class DeadLetterQueueHelper + { + internal static bool IsDefined(DeadLetterQueue value) + { + return value == DeadLetterQueue.None + || value == DeadLetterQueue.System + || value == DeadLetterQueue.Custom; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MessageSecurityOverMsmq.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MessageSecurityOverMsmq.cs new file mode 100644 index 00000000000..76001ad3c85 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MessageSecurityOverMsmq.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Security; + +namespace System.ServiceModel +{ + public sealed class MessageSecurityOverMsmq + { + internal const MessageCredentialType DefaultClientCredentialType = MessageCredentialType.Windows; + + private MessageCredentialType _clientCredentialType; + private SecurityAlgorithmSuite _algorithmSuite; + private bool _wasAlgorithmSuiteSet; + + public MessageSecurityOverMsmq() + { + _clientCredentialType = DefaultClientCredentialType; + _algorithmSuite = SecurityAlgorithmSuite.Default; + } + + [DefaultValue(MsmqDefaults.DefaultClientCredentialType)] + public MessageCredentialType ClientCredentialType + { + get { return _clientCredentialType; } + set + { + if (!IsMessageCredentialTypeDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _clientCredentialType = value; + } + } + + public SecurityAlgorithmSuite AlgorithmSuite + { + get { return _algorithmSuite; } + set + { + _algorithmSuite = value ?? throw new ArgumentNullException(nameof(value)); + _wasAlgorithmSuiteSet = true; + } + } + + internal bool WasAlgorithmSuiteSet + { + get { return _wasAlgorithmSuiteSet; } + } + + private static bool IsMessageCredentialTypeDefined(MessageCredentialType value) + { + return value == MessageCredentialType.None + || value == MessageCredentialType.Windows + || value == MessageCredentialType.UserName + || value == MessageCredentialType.Certificate + || value == MessageCredentialType.IssuedToken; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqAuthenticationMode.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqAuthenticationMode.cs new file mode 100644 index 00000000000..db2282801d0 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqAuthenticationMode.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel +{ + public enum MsmqAuthenticationMode + { + None, + WindowsDomain, + Certificate, + } + + internal static class MsmqAuthenticationModeHelper + { + public static bool IsDefined(MsmqAuthenticationMode mode) + { + return mode >= MsmqAuthenticationMode.None && mode <= MsmqAuthenticationMode.Certificate; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqBindingBase.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqBindingBase.cs new file mode 100644 index 00000000000..4fde4eb8580 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqBindingBase.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.ServiceModel.Channels; + +namespace System.ServiceModel +{ + public abstract class MsmqBindingBase : Binding, IBindingRuntimePreferences + { + internal MsmqBindingElementBase transport; + + protected MsmqBindingBase() + { + } + + public Uri CustomDeadLetterQueue + { + get { return transport.CustomDeadLetterQueue; } + set { transport.CustomDeadLetterQueue = value; } + } + + [DefaultValue(MsmqDefaults.DeadLetterQueue)] + public DeadLetterQueue DeadLetterQueue + { + get { return transport.DeadLetterQueue; } + set { transport.DeadLetterQueue = value; } + } + + [DefaultValue(MsmqDefaults.Durable)] + public bool Durable + { + get { return transport.Durable; } + set { transport.Durable = value; } + } + + [DefaultValue(MsmqDefaults.ExactlyOnce)] + public bool ExactlyOnce + { + get { return transport.ExactlyOnce; } + set { transport.ExactlyOnce = value; } + } + + public long MaxReceivedMessageSize + { + get { return transport.MaxReceivedMessageSize; } + set { transport.MaxReceivedMessageSize = value; } + } + + [DefaultValue(MsmqDefaults.ReceiveRetryCount)] + public int ReceiveRetryCount + { + get { return transport.ReceiveRetryCount; } + set { transport.ReceiveRetryCount = value; } + } + + [DefaultValue(MsmqDefaults.MaxRetryCycles)] + public int MaxRetryCycles + { + get { return transport.MaxRetryCycles; } + set { transport.MaxRetryCycles = value; } + } + + [DefaultValue(MsmqDefaults.ReceiveContextEnabled)] + public bool ReceiveContextEnabled + { + get { return transport.ReceiveContextEnabled; } + set { transport.ReceiveContextEnabled = value; } + } + + [DefaultValue(MsmqDefaults.ReceiveErrorHandling)] + public ReceiveErrorHandling ReceiveErrorHandling + { + get { return transport.ReceiveErrorHandling; } + set { transport.ReceiveErrorHandling = value; } + } + + [DefaultValue(typeof(TimeSpan), MsmqDefaults.RetryCycleDelayString)] + public TimeSpan RetryCycleDelay + { + get { return transport.RetryCycleDelay; } + set { transport.RetryCycleDelay = value; } + } + + public override string Scheme => transport.Scheme; + + [DefaultValue(typeof(TimeSpan), MsmqDefaults.TimeToLiveString)] + public TimeSpan TimeToLive + { + get { return transport.TimeToLive; } + set { transport.TimeToLive = value; } + } + + [DefaultValue(MsmqDefaults.UseSourceJournal)] + public bool UseSourceJournal + { + get { return transport.UseSourceJournal; } + set { transport.UseSourceJournal = value; } + } + + [DefaultValue(MsmqDefaults.UseMsmqTracing)] + public bool UseMsmqTracing + { + get { return transport.UseMsmqTracing; } + set { transport.UseMsmqTracing = value; } + } + + bool IBindingRuntimePreferences.ReceiveSynchronously => ExactlyOnce; + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqEncryptionAlgorithm.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqEncryptionAlgorithm.cs new file mode 100644 index 00000000000..41a7e18c4e3 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqEncryptionAlgorithm.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel +{ + public enum MsmqEncryptionAlgorithm + { + RC4Stream, + Aes + } + + internal static class MsmqEncryptionAlgorithmHelper + { + public static bool IsDefined(MsmqEncryptionAlgorithm algorithm) + { + return algorithm == MsmqEncryptionAlgorithm.RC4Stream || algorithm == MsmqEncryptionAlgorithm.Aes; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqException.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqException.cs new file mode 100644 index 00000000000..a1a34b60e0a --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqException.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.ServiceModel.Channels; + +namespace System.ServiceModel +{ + // Wraps MSMQ native error codes (MQ_ERROR_*) returned from the + // underlying queue. Maps them to standard WCF exception types via + // the Normalized property so callers see meaningful exceptions + // (EndpointNotFoundException, TimeoutException, ...) instead of a + // raw integer error. + [Serializable] + public class MsmqException : ExternalException + { + [NonSerialized] private bool? _faultSender; + [NonSerialized] private bool? _faultReceiver; + [NonSerialized] private Type _outerExceptionType; + + public MsmqException() { } + public MsmqException(string message) : base(message) { } + public MsmqException(string message, int error) : base(message, error) { } + public MsmqException(string message, Exception inner) : base(message, inner) { } +#pragma warning disable SYSLIB0051 // legacy formatter-based serialization support + protected MsmqException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 + + internal bool FaultSender + { + get + { + TuneBehavior(); + return _faultSender.Value; + } + } + + internal bool FaultReceiver + { + get + { + TuneBehavior(); + return _faultReceiver.Value; + } + } + + // Returns the most appropriate WCF exception for this MSMQ error, + // wrapping `this` as the inner exception. Used by the channel + // factory to surface a standard exception (e.g. + // EndpointNotFoundException for QueueNotFound) instead of leaking + // the raw integer error code to callers. + internal Exception Normalized + { + get + { + TuneBehavior(); + if (_outerExceptionType == null) + { + return this; + } + return (Exception)Activator.CreateInstance(_outerExceptionType, new object[] { Message, this }); + } + } + + // Exposed internally to make the WCF-style exception type + // available to channel code without taking a runtime call on + // every send. Tests reach the property by reflection. + internal Type NormalizedType + { + get + { + TuneBehavior(); + return _outerExceptionType; + } + } + + private void TuneBehavior() + { + if (_faultSender.HasValue && _faultReceiver.HasValue) + { + return; + } + + // Mirrors the (int -> behavior) table from the .NET Framework + // reference source. Codes are the native MQ_ERROR_* values. + switch (unchecked((uint)ErrorCode)) + { + // ----- Configuration errors ----- + case MsmqErrorCodes.AccessDenied: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(AddressAccessDeniedException); break; + case MsmqErrorCodes.QueueDeleted: + case MsmqErrorCodes.QueueNotFound: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(EndpointNotFoundException); break; + case MsmqErrorCodes.IllegalFormatName: + case MsmqErrorCodes.IllegalQueuePathName: + _faultSender = false; _faultReceiver = false; _outerExceptionType = typeof(ArgumentException); break; + case MsmqErrorCodes.UnsupportedFormatNameOperation: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(ArgumentException); break; + case MsmqErrorCodes.SharingViolation: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(AddressAccessDeniedException); break; + + // ----- Transient errors ----- + case MsmqErrorCodes.IOTimeout: + _faultSender = false; _faultReceiver = false; _outerExceptionType = typeof(TimeoutException); break; + case MsmqErrorCodes.QueueNotAvailable: + case MsmqErrorCodes.RemoteMachineNotAvailable: + case MsmqErrorCodes.ServiceNotAvailable: + _faultSender = false; _faultReceiver = true; _outerExceptionType = typeof(EndpointNotFoundException); break; + case MsmqErrorCodes.InsufficientResources: + case MsmqErrorCodes.MessageStorageFailed: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(CommunicationException); break; + case MsmqErrorCodes.TransactionEnlist: + _faultSender = false; _faultReceiver = true; _outerExceptionType = typeof(CommunicationException); break; + case MsmqErrorCodes.TransactionImport: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(CommunicationException); break; + case MsmqErrorCodes.TransactionUsage: + _faultSender = true; _faultReceiver = true; _outerExceptionType = typeof(InvalidOperationException); break; + case MsmqErrorCodes.StaleHandle: + _faultSender = false; _faultReceiver = false; _outerExceptionType = typeof(InvalidOperationException); break; + + default: + _faultSender = true; _faultReceiver = true; _outerExceptionType = null; break; + } + } + } + + // MQ_ERROR_* native MSMQ error codes (a subset that's normalized by + // MsmqException). Values are exact matches for the native winerror + // codes used by mqrt.dll, and also for MSMQ.Messaging's + // MessageQueueErrorCode enum members of the same name. + internal static class MsmqErrorCodes + { + internal const uint AccessDenied = 0xC00E0025; + internal const uint QueueNotFound = 0xC00E0003; + internal const uint QueueDeleted = 0xC00E005A; + internal const uint IllegalFormatName = 0xC00E001E; + internal const uint IllegalQueuePathName = 0xC00E0014; + internal const uint UnsupportedFormatNameOperation = 0xC00E0020; + internal const uint SharingViolation = 0xC00E0009; + internal const uint IOTimeout = 0xC00E001B; + internal const uint QueueNotAvailable = 0xC00E0026; + internal const uint RemoteMachineNotAvailable = 0xC00E000E; + internal const uint ServiceNotAvailable = 0xC00E000B; + internal const uint InsufficientResources = 0xC00E0027; + internal const uint MessageStorageFailed = 0xC00E002A; + internal const uint TransactionEnlist = 0xC00E0058; + internal const uint TransactionImport = 0xC00E0051; + internal const uint TransactionUsage = 0xC00E0050; + internal const uint StaleHandle = 0xC00E0006; + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/AcknowledgeTypes.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/AcknowledgeTypes.cs new file mode 100644 index 00000000000..05d93fd8147 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/AcknowledgeTypes.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.MsmqIntegration +{ + // Mirrors System.Messaging.AcknowledgeTypes. Values correspond to + // the MQMSG_ACKNOWLEDGMENT_* flags consumed by MSMQ's native API. + [Flags] + public enum AcknowledgeTypes + { + None = 0, + PositiveArrival = 1, + PositiveReceive = 2, + NegativeArrival = 4, + NotAcknowledgeReachQueue = 4, + NegativeReceive = 8, + NotAcknowledgeReceive = 12, + FullReachQueue = 5, + FullReceive = 14, + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/Acknowledgment.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/Acknowledgment.cs new file mode 100644 index 00000000000..53ac3353416 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/Acknowledgment.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.MsmqIntegration +{ + // Mirrors System.Messaging.Acknowledgment. Values correspond to the + // MQMSG_CLASS_* classifications set by MSMQ on incoming admin + // messages to indicate the disposition of a previously sent + // message. Never set by user code; surfaced via + // MsmqMessage.Acknowledgment. + public enum Acknowledgment + { + None = 0, + ReachQueue = 0x0002, + Receive = 0x4000, + QueuePurged = 0x4001, + ReceiveTimeout = 0x4002, + ReachQueueTimeout = 0x4003, + ReceiveRejected = 0x4005, + BadDestinationQueue = 0x8000, + Purged = 0x8001, + QueueExceedMaximumSize = 0x8002, + AccessDenied = 0x8004, + HopCountExceeded = 0x8005, + BadSignature = 0x8006, + BadEncryption = 0x8007, + CouldNotEncrypt = 0x8008, + NotTransactionalQueue = 0x8009, + NotTransactionalMessage = 0x800A, + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MessagePriority.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MessagePriority.cs new file mode 100644 index 00000000000..3a9dafe2b13 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MessagePriority.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.MsmqIntegration +{ + // Mirrors System.Messaging.MessagePriority. Higher-priority + // messages are delivered ahead of lower-priority ones within a + // single queue. The default at the MSMQ level is Normal. + public enum MessagePriority + { + Lowest = 0, + VeryLow = 1, + Low = 2, + Normal = 3, + AboveNormal = 4, + High = 5, + VeryHigh = 6, + Highest = 7, + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MessageType.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MessageType.cs new file mode 100644 index 00000000000..2d989b39d44 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MessageType.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.MsmqIntegration +{ + // Mirrors System.Messaging.MessageType. Indicates whether an + // incoming message is a normal application payload, a response to + // a previous send, a system report, or an acknowledgment message + // emitted by MSMQ in response to a requested AcknowledgeTypes flag. + public enum MessageType + { + Normal = 1, + Response = 2, + Report = 3, + Acknowledgment = 6, + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationBinding.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationBinding.cs new file mode 100644 index 00000000000..7f15d8b2795 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationBinding.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.ServiceModel.Channels; + +namespace System.ServiceModel.MsmqIntegration +{ + public class MsmqIntegrationBinding : MsmqBindingBase + { + private MsmqIntegrationSecurity _security = new MsmqIntegrationSecurity(); + + public MsmqIntegrationBinding() + { + Initialize(); + } + + public MsmqIntegrationBinding(MsmqIntegrationSecurityMode securityMode) + { + if (!MsmqIntegrationSecurityModeHelper.IsDefined(securityMode)) + { + throw new InvalidEnumArgumentException(nameof(securityMode), (int)securityMode, typeof(MsmqIntegrationSecurityMode)); + } + Initialize(); + _security.Mode = securityMode; + } + + public MsmqIntegrationSecurity Security + { + get { return _security; } + set { _security = value; } + } + + internal Type[] TargetSerializationTypes + { + get { return ((MsmqIntegrationBindingElement)transport).TargetSerializationTypes; } + set { ((MsmqIntegrationBindingElement)transport).TargetSerializationTypes = value; } + } + + [DefaultValue(MsmqDefaults.MsmqMessageSerializationFormat)] + public MsmqMessageSerializationFormat SerializationFormat + { + get { return ((MsmqIntegrationBindingElement)transport).SerializationFormat; } + set { ((MsmqIntegrationBindingElement)transport).SerializationFormat = value; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeSecurity() + { + if (_security.Mode != MsmqIntegrationSecurityMode.Transport) + { + return true; + } + if (_security.Transport.MsmqAuthenticationMode != MsmqDefaults.MsmqAuthenticationMode + || _security.Transport.MsmqEncryptionAlgorithm != MsmqDefaults.MsmqEncryptionAlgorithm + || _security.Transport.MsmqSecureHashAlgorithm != MsmqDefaults.MsmqSecureHashAlgorithm + || _security.Transport.MsmqProtectionLevel != MsmqDefaults.MsmqProtectionLevel) + { + return true; + } + return false; + } + + private void Initialize() + { + transport = new MsmqIntegrationBindingElement(); + } + + public override BindingElementCollection CreateBindingElements() + { + var bindingElements = new BindingElementCollection(); + _security.ConfigureTransportSecurity(transport); + bindingElements.Add(transport); + return bindingElements.Clone(); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationBindingElement.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationBindingElement.cs new file mode 100644 index 00000000000..71c7f42cda7 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationBindingElement.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.ServiceModel.Channels; + +namespace System.ServiceModel.MsmqIntegration +{ + public sealed class MsmqIntegrationBindingElement : MsmqBindingElementBase + { + private MsmqMessageSerializationFormat _serializationFormat; + private Type[] _targetSerializationTypes; + + public MsmqIntegrationBindingElement() + { + _serializationFormat = MsmqDefaults.MsmqMessageSerializationFormat; + } + + private MsmqIntegrationBindingElement(MsmqIntegrationBindingElement other) + : base(other) + { + _serializationFormat = other._serializationFormat; + if (other._targetSerializationTypes != null) + { + _targetSerializationTypes = (Type[])other._targetSerializationTypes.Clone(); + } + } + + public override string Scheme => "msmq.formatname"; + + internal override MsmqUri.IAddressTranslator AddressTranslator + { + // Filled in by the slice that ports MsmqUri.FormatNameAddressTranslator. + get { return null; } + } + + public MsmqMessageSerializationFormat SerializationFormat + { + get { return _serializationFormat; } + set + { + if (!MsmqMessageSerializationFormatHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _serializationFormat = value; + } + } + + public Type[] TargetSerializationTypes + { + get + { + return _targetSerializationTypes == null + ? null + : (Type[])_targetSerializationTypes.Clone(); + } + set + { + _targetSerializationTypes = value == null + ? null + : (Type[])value.Clone(); + } + } + + public override BindingElement Clone() + { + return new MsmqIntegrationBindingElement(this); + } + + public override bool CanBuildChannelFactory(BindingContext context) + { + return typeof(TChannel) == typeof(IOutputChannel); + } + + [SupportedOSPlatform("windows")] + public override IChannelFactory BuildChannelFactory(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (typeof(TChannel) != typeof(IOutputChannel)) + { + throw new ArgumentException(SR.Format(SR.ChannelTypeNotSupported, typeof(TChannel)), "TChannel"); + } + return (IChannelFactory)(object)new MsmqIntegrationOutputChannelFactory(this, context); + } + + public override T GetProperty(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (typeof(T) == typeof(MessageVersion)) + { + return (T)(object)MessageVersion.None; + } + return base.GetProperty(context); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationMessageProperty.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationMessageProperty.cs new file mode 100644 index 00000000000..1dd3486bdcf --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationMessageProperty.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.ServiceModel.Channels; + +namespace System.ServiceModel.MsmqIntegration +{ + // Carries MSMQ message metadata (label, priority, correlation, …) + // alongside the message body. Mirrors the netfx public type so + // applications porting from .NET Framework can keep their usage + // unchanged. The enums (AcknowledgeTypes, Acknowledgment, + // MessageType, MessagePriority) live in this same namespace; their + // values match the MQMSG_* / MQMSG_CLASS_* native constants + // exactly so they round-trip through the underlying MSMQ ABI + // without conversion loss. + [SupportedOSPlatform("windows")] + public sealed class MsmqIntegrationMessageProperty + { + public const string Name = "MsmqIntegrationMessageProperty"; + + public object Body { get; set; } + public AcknowledgeTypes? AcknowledgeType { get; set; } + public Acknowledgment? Acknowledgment { get; internal set; } + public Uri AdministrationQueue { get; set; } + public int? AppSpecific { get; set; } + public DateTime? ArrivedTime { get; internal set; } + public bool? Authenticated { get; internal set; } + public int? BodyType { get; set; } + public string CorrelationId { get; set; } + public Uri DestinationQueue { get; internal set; } + public byte[] Extension { get; set; } + public string Id { get; internal set; } + public string Label { get; set; } + public MessageType? MessageType { get; internal set; } + public MessagePriority? Priority { get; set; } + public Uri ResponseQueue { get; set; } + public byte[] SenderId { get; internal set; } + public DateTime? SentTime { get; internal set; } + public TimeSpan? TimeToReachQueue { get; set; } + + public static MsmqIntegrationMessageProperty Get(System.ServiceModel.Channels.Message message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + return message.Properties.TryGetValue(Name, out object value) ? value as MsmqIntegrationMessageProperty : null; + } + + // Copies the user-settable MSMQ properties onto the outgoing + // native message. Each property maps to a PROPID_M_* slot whose + // VT_* matches the value's natural width: + // + // AcknowledgeType VT_UI1 PROPID_M_ACKNOWLEDGE + // AdministrationQueue VT_LPWSTR + VT_UI4 (length) + // AppSpecific VT_UI4 PROPID_M_APPSPECIFIC + // BodyType VT_UI4 PROPID_M_BODY_TYPE + // CorrelationId VT_VECTOR|VT_UI1 (20 bytes) + // Extension VT_VECTOR|VT_UI1 + VT_UI4 (length) + // Label VT_LPWSTR + VT_UI4 (length) + // Priority VT_UI1 PROPID_M_PRIORITY + // ResponseQueue VT_LPWSTR + VT_UI4 (length) + // TimeToReachQueue VT_UI4 PROPID_M_TIME_TO_REACH_QUEUE + internal void ApplyTo(NativeMsmqMessage message) + { + if (AcknowledgeType.HasValue) + { + message.SetByte(UnsafeNativeMethods.PROPID_M_ACKNOWLEDGE, (byte)(int)AcknowledgeType.Value); + } + if (AdministrationQueue != null) + { + message.SetWideString( + UnsafeNativeMethods.PROPID_M_ADMIN_QUEUE, + MsmqUri.UriToFormatNameByScheme(AdministrationQueue), + UnsafeNativeMethods.PROPID_M_ADMIN_QUEUE_LEN); + } + if (AppSpecific.HasValue) + { + message.SetUInt32(UnsafeNativeMethods.PROPID_M_APPSPECIFIC, unchecked((uint)AppSpecific.Value)); + } + if (BodyType.HasValue) + { + message.SetUInt32(UnsafeNativeMethods.PROPID_M_BODY_TYPE, unchecked((uint)BodyType.Value)); + } + if (!string.IsNullOrEmpty(CorrelationId)) + { + message.SetByteVector( + UnsafeNativeMethods.PROPID_M_CORRELATIONID, + NativeMsmqMessage.ParseMessageId(CorrelationId), + lengthPropId: 0); + } + if (Extension != null) + { + message.SetByteVector( + UnsafeNativeMethods.PROPID_M_EXTENSION, + Extension, + UnsafeNativeMethods.PROPID_M_EXTENSION_LEN); + } + if (!string.IsNullOrEmpty(Label)) + { + message.SetWideString( + UnsafeNativeMethods.PROPID_M_LABEL, + Label, + UnsafeNativeMethods.PROPID_M_LABEL_LEN); + } + if (Priority.HasValue) + { + message.SetByte(UnsafeNativeMethods.PROPID_M_PRIORITY, (byte)(int)Priority.Value); + } + if (ResponseQueue != null) + { + message.SetWideString( + UnsafeNativeMethods.PROPID_M_RESP_QUEUE, + MsmqUri.UriToFormatNameByScheme(ResponseQueue), + UnsafeNativeMethods.PROPID_M_RESP_QUEUE_LEN); + } + if (TimeToReachQueue.HasValue) + { + message.SetTimeToReachQueue(TimeToReachQueue.Value); + } + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationOutputChannel.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationOutputChannel.cs new file mode 100644 index 00000000000..8e3f5a9a325 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationOutputChannel.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.ServiceModel.Channels; +using System.Threading.Tasks; + +namespace System.ServiceModel.MsmqIntegration +{ + // Send-side channel for MsmqIntegrationBinding. Targets classic MSMQ + // applications that exchange raw payloads (no SOAP envelope). The + // channel writes the encoded WCF message bytes directly to MSMQ as + // the message body. Full MsmqMessage / MsmqIntegrationMessageProperty + // support (priority, label, ack types, etc.) lands in a follow-up + // slice that ports the netfx integration message-property layer. + [SupportedOSPlatform("windows")] + internal sealed class MsmqIntegrationOutputChannel : ChannelBase, IOutputChannel + { + private readonly MsmqIntegrationOutputChannelFactory _factory; + private readonly EndpointAddress _remoteAddress; + private readonly Uri _via; + private readonly string _formatName; + + internal MsmqIntegrationOutputChannel(MsmqIntegrationOutputChannelFactory factory, EndpointAddress remoteAddress, Uri via) + : base(factory) + { + _factory = factory; + _remoteAddress = remoteAddress; + _via = via; + _formatName = MsmqUri.UriToFormatNameByScheme(via); + } + + public EndpointAddress RemoteAddress => _remoteAddress; + + public Uri Via => _via; + + public void Send(Message message) => Send(message, DefaultSendTimeout); + + public void Send(Message message, TimeSpan timeout) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + ThrowIfDisposedOrNotOpen(); + + ArraySegment encoded = _factory.MessageEncoder.WriteMessage( + message, int.MaxValue, _factory.BufferManager); + try + { + MsmqIntegrationMessageProperty property = MsmqIntegrationMessageProperty.Get(message); + MsmqMessagingInterop.Send( + _formatName, + encoded.Array, + encoded.Offset, + encoded.Count, + property, + _factory.BindingElement.ExactlyOnce, + _factory.BindingElement.TimeToLive, + timeout); + } + finally + { + _factory.BufferManager.ReturnBuffer(encoded.Array); + } + } + + public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state) + => BeginSend(message, DefaultSendTimeout, callback, state); + + public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state) + => Task.Run(() => Send(message, timeout)).ToApm(callback, state); + + public void EndSend(IAsyncResult result) => result.ToApmEnd(); + + protected override void OnAbort() { } + + protected override void OnClose(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndClose(IAsyncResult result) => result.ToApmEnd(); + + protected override void OnOpen(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndOpen(IAsyncResult result) => result.ToApmEnd(); + + protected override Task OnOpenAsync(TimeSpan timeout) => Task.CompletedTask; + + protected override Task OnCloseAsync(TimeSpan timeout) => Task.CompletedTask; + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationOutputChannelFactory.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationOutputChannelFactory.cs new file mode 100644 index 00000000000..e9f6a60d19d --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationOutputChannelFactory.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; +using System.ServiceModel.Channels; +using System.Threading.Tasks; + +namespace System.ServiceModel.MsmqIntegration +{ + [SupportedOSPlatform("windows")] + internal sealed class MsmqIntegrationOutputChannelFactory : ChannelFactoryBase + { + private readonly MsmqIntegrationBindingElement _bindingElement; + private readonly MessageEncoderFactory _messageEncoderFactory; + private readonly BufferManager _bufferManager; + + internal MsmqIntegrationOutputChannelFactory(MsmqIntegrationBindingElement bindingElement, BindingContext context) + : base(context.Binding) + { + _bindingElement = bindingElement ?? throw new ArgumentNullException(nameof(bindingElement)); + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + // MsmqIntegrationBinding carries raw payloads (MessageVersion.None), so + // pick whatever encoder the binding context supplied; default to binary + // when nothing is configured. The encoder serializes the body bytes that + // get dispatched to MSMQ as the message payload. + MessageEncodingBindingElement encodingElement = + context.BindingParameters.Find() + ?? new BinaryMessageEncodingBindingElement(); + _messageEncoderFactory = encodingElement.CreateMessageEncoderFactory(); + _bufferManager = BufferManager.CreateBufferManager( + bindingElement.MaxBufferPoolSize, + (int)Math.Min(bindingElement.MaxReceivedMessageSize, int.MaxValue)); + } + + internal MsmqIntegrationBindingElement BindingElement => _bindingElement; + internal MessageEncoder MessageEncoder => _messageEncoderFactory.Encoder; + internal BufferManager BufferManager => _bufferManager; + + public override T GetProperty() + { + if (typeof(T) == typeof(MessageVersion)) + { + return (T)(object)MessageVersion.None; + } + return base.GetProperty(); + } + + protected override IOutputChannel OnCreateChannel(EndpointAddress address, Uri via) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + return new MsmqIntegrationOutputChannel(this, address, via ?? address.Uri); + } + + protected override void OnAbort() => _bufferManager?.Clear(); + + protected override void OnClose(TimeSpan timeout) => _bufferManager?.Clear(); + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndClose(IAsyncResult result) + { + _bufferManager?.Clear(); + result.ToApmEnd(); + } + + protected override void OnOpen(TimeSpan timeout) { } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + => Task.CompletedTask.ToApm(callback, state); + + protected override void OnEndOpen(IAsyncResult result) => result.ToApmEnd(); + + protected override Task OnOpenAsync(TimeSpan timeout) => Task.CompletedTask; + + protected override Task OnCloseAsync(TimeSpan timeout) + { + _bufferManager?.Clear(); + return Task.CompletedTask; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationSecurity.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationSecurity.cs new file mode 100644 index 00000000000..a09180e5422 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationSecurity.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.ServiceModel.Channels; + +namespace System.ServiceModel.MsmqIntegration +{ + public sealed class MsmqIntegrationSecurity + { + internal const MsmqIntegrationSecurityMode DefaultMode = MsmqIntegrationSecurityMode.Transport; + + private MsmqIntegrationSecurityMode _mode; + private MsmqTransportSecurity _transportSecurity; + + public MsmqIntegrationSecurity() + { + _mode = DefaultMode; + _transportSecurity = new MsmqTransportSecurity(); + } + + [DefaultValue(DefaultMode)] + public MsmqIntegrationSecurityMode Mode + { + get { return _mode; } + set + { + if (!MsmqIntegrationSecurityModeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _mode = value; + } + } + + public MsmqTransportSecurity Transport + { + get { return _transportSecurity; } + set { _transportSecurity = value; } + } + + internal void ConfigureTransportSecurity(MsmqBindingElementBase msmq) + { + if (_mode == MsmqIntegrationSecurityMode.Transport) + { + msmq.MsmqTransportSecurity = Transport; + } + else + { + msmq.MsmqTransportSecurity.Disable(); + } + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationSecurityMode.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationSecurityMode.cs new file mode 100644 index 00000000000..9ff088fc773 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqIntegrationSecurityMode.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.MsmqIntegration +{ + public enum MsmqIntegrationSecurityMode + { + None, + Transport + } + + internal static class MsmqIntegrationSecurityModeHelper + { + internal static bool IsDefined(MsmqIntegrationSecurityMode value) + { + return (value == MsmqIntegrationSecurityMode.Transport + || value == MsmqIntegrationSecurityMode.None); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqMessage.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqMessage.cs new file mode 100644 index 00000000000..9478534430f --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqMessage.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Versioning; + +namespace System.ServiceModel.MsmqIntegration +{ + // Wrapper around a single MSMQ message body of type T plus the MSMQ + // metadata exposed via MsmqIntegrationMessageProperty. Mirrors the + // netfx public type — applications porting from .NET Framework can + // keep their MsmqMessage usage unchanged. + [SupportedOSPlatform("windows")] + public sealed class MsmqMessage + { + private readonly MsmqIntegrationMessageProperty _property; + + public MsmqMessage(T body) + { + if (body == null) + { + throw new ArgumentNullException(nameof(body)); + } + _property = new MsmqIntegrationMessageProperty { Body = body }; + } + + internal MsmqMessage() + { + _property = new MsmqIntegrationMessageProperty(); + } + + internal MsmqIntegrationMessageProperty Property => _property; + + public T Body + { + get { return (T)_property.Body; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + _property.Body = value; + } + } + + public AcknowledgeTypes? AcknowledgeType + { + get => _property.AcknowledgeType; + set => _property.AcknowledgeType = value; + } + + public Acknowledgment? Acknowledgment => _property.Acknowledgment; + + public Uri AdministrationQueue + { + get => _property.AdministrationQueue; + set => _property.AdministrationQueue = value; + } + + public int? AppSpecific + { + get => _property.AppSpecific; + set => _property.AppSpecific = value; + } + + public DateTime? ArrivedTime => _property.ArrivedTime; + public bool? Authenticated => _property.Authenticated; + + public int? BodyType + { + get => _property.BodyType; + set => _property.BodyType = value; + } + + public string CorrelationId + { + get => _property.CorrelationId; + set => _property.CorrelationId = value; + } + + public Uri DestinationQueue => _property.DestinationQueue; + + public byte[] Extension + { + get => _property.Extension; + set => _property.Extension = value; + } + + public string Id => _property.Id; + + public string Label + { + get => _property.Label; + set => _property.Label = value; + } + + public MessageType? MessageType => _property.MessageType; + + public MessagePriority? Priority + { + get => _property.Priority; + set => _property.Priority = value; + } + + public Uri ResponseQueue + { + get => _property.ResponseQueue; + set => _property.ResponseQueue = value; + } + + public byte[] SenderId => _property.SenderId; + public DateTime? SentTime => _property.SentTime; + + public TimeSpan? TimeToReachQueue + { + get => _property.TimeToReachQueue; + set => _property.TimeToReachQueue = value; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqMessageSerializationFormat.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqMessageSerializationFormat.cs new file mode 100644 index 00000000000..522e9a2efed --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqIntegration/MsmqMessageSerializationFormat.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel.MsmqIntegration +{ + public enum MsmqMessageSerializationFormat + { + Xml, + Binary, + ActiveX, + ByteArray, + Stream + } + + internal static class MsmqMessageSerializationFormatHelper + { + internal static bool IsDefined(MsmqMessageSerializationFormat value) + { + return + value == MsmqMessageSerializationFormat.ActiveX || + value == MsmqMessageSerializationFormat.Binary || + value == MsmqMessageSerializationFormat.ByteArray || + value == MsmqMessageSerializationFormat.Stream || + value == MsmqMessageSerializationFormat.Xml; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqPoisonMessageException.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqPoisonMessageException.cs new file mode 100644 index 00000000000..b249df37773 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqPoisonMessageException.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Serialization; + +namespace System.ServiceModel +{ + [Serializable] + public class MsmqPoisonMessageException : PoisonMessageException + { + private long _messageLookupId = 0; + + public MsmqPoisonMessageException() { } + public MsmqPoisonMessageException(string message) : base(message) { } + public MsmqPoisonMessageException(string message, Exception innerException) : base(message, innerException) { } + public MsmqPoisonMessageException(long messageLookupId) : this(messageLookupId, null) { } + public MsmqPoisonMessageException(long messageLookupId, Exception innerException) + : base(SR.MsmqPoisonMessage, innerException) + { + _messageLookupId = messageLookupId; + } + + public long MessageLookupId + { + get { return _messageLookupId; } + } + + protected MsmqPoisonMessageException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _messageLookupId = (long)info.GetValue("messageLookupId", typeof(long)); + } + +#pragma warning disable SYSLIB0051 // Type or member is obsolete: legacy formatter-based serialization support. + [Obsolete] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("messageLookupId", _messageLookupId); + } +#pragma warning restore SYSLIB0051 + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqSecureHashAlgorithm.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqSecureHashAlgorithm.cs new file mode 100644 index 00000000000..86e01649fde --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqSecureHashAlgorithm.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel +{ + public enum MsmqSecureHashAlgorithm + { + MD5, + Sha1, + Sha256, + Sha512 + } + + internal static class MsmqSecureHashAlgorithmHelper + { + public static bool IsDefined(MsmqSecureHashAlgorithm algorithm) + { + return algorithm == MsmqSecureHashAlgorithm.MD5 || + algorithm == MsmqSecureHashAlgorithm.Sha1 || + algorithm == MsmqSecureHashAlgorithm.Sha256 || + algorithm == MsmqSecureHashAlgorithm.Sha512; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqTransportSecurity.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqTransportSecurity.cs new file mode 100644 index 00000000000..ae0963efa15 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/MsmqTransportSecurity.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.Net.Security; +using System.ServiceModel.Channels; + +namespace System.ServiceModel +{ + public sealed class MsmqTransportSecurity + { + private MsmqAuthenticationMode _msmqAuthenticationMode; + private MsmqEncryptionAlgorithm _msmqEncryptionAlgorithm; + private MsmqSecureHashAlgorithm _msmqHashAlgorithm; + private ProtectionLevel _msmqProtectionLevel; + + public MsmqTransportSecurity() + { + _msmqAuthenticationMode = MsmqDefaults.MsmqAuthenticationMode; + _msmqEncryptionAlgorithm = MsmqDefaults.MsmqEncryptionAlgorithm; + _msmqHashAlgorithm = MsmqDefaults.MsmqSecureHashAlgorithm; + _msmqProtectionLevel = MsmqDefaults.MsmqProtectionLevel; + } + + public MsmqTransportSecurity(MsmqTransportSecurity other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + _msmqAuthenticationMode = other.MsmqAuthenticationMode; + _msmqEncryptionAlgorithm = other.MsmqEncryptionAlgorithm; + _msmqHashAlgorithm = other.MsmqSecureHashAlgorithm; + _msmqProtectionLevel = other.MsmqProtectionLevel; + } + + internal bool Enabled + { + get + { + return _msmqAuthenticationMode != MsmqAuthenticationMode.None + && _msmqProtectionLevel != ProtectionLevel.None; + } + } + + [DefaultValue(MsmqDefaults.MsmqAuthenticationMode)] + public MsmqAuthenticationMode MsmqAuthenticationMode + { + get { return _msmqAuthenticationMode; } + set + { + if (!MsmqAuthenticationModeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _msmqAuthenticationMode = value; + } + } + + [DefaultValue(MsmqDefaults.MsmqEncryptionAlgorithm)] + public MsmqEncryptionAlgorithm MsmqEncryptionAlgorithm + { + get { return _msmqEncryptionAlgorithm; } + set + { + if (!MsmqEncryptionAlgorithmHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _msmqEncryptionAlgorithm = value; + } + } + + [DefaultValue(MsmqDefaults.DefaultMsmqSecureHashAlgorithm)] + public MsmqSecureHashAlgorithm MsmqSecureHashAlgorithm + { + get { return _msmqHashAlgorithm; } + set + { + if (!MsmqSecureHashAlgorithmHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _msmqHashAlgorithm = value; + } + } + + [DefaultValue(MsmqDefaults.MsmqProtectionLevel)] + public ProtectionLevel MsmqProtectionLevel + { + get { return _msmqProtectionLevel; } + set + { + if (!IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _msmqProtectionLevel = value; + } + } + + internal void Disable() + { + _msmqAuthenticationMode = MsmqAuthenticationMode.None; + _msmqProtectionLevel = ProtectionLevel.None; + } + + private static bool IsDefined(ProtectionLevel value) + { + return value == ProtectionLevel.None + || value == ProtectionLevel.Sign + || value == ProtectionLevel.EncryptAndSign; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqBinding.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqBinding.cs new file mode 100644 index 00000000000..c3dddc65857 --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqBinding.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.ServiceModel.Channels; +using System.Xml; + +namespace System.ServiceModel +{ + public class NetMsmqBinding : MsmqBindingBase + { + private BinaryMessageEncodingBindingElement _encoding; + private NetMsmqSecurity _security; + + public NetMsmqBinding() + { + Initialize(); + _security = new NetMsmqSecurity(); + } + + public NetMsmqBinding(NetMsmqSecurityMode securityMode) + { + if (!NetMsmqSecurityModeHelper.IsDefined(securityMode)) + { + throw new InvalidEnumArgumentException(nameof(securityMode), (int)securityMode, typeof(NetMsmqSecurityMode)); + } + Initialize(); + _security = new NetMsmqSecurity(securityMode); + } + + [DefaultValue(MsmqDefaults.QueueTransferProtocol)] + public QueueTransferProtocol QueueTransferProtocol + { + get { return ((MsmqTransportBindingElement)transport).QueueTransferProtocol; } + set { ((MsmqTransportBindingElement)transport).QueueTransferProtocol = value; } + } + + public XmlDictionaryReaderQuotas ReaderQuotas + { + get { return _encoding.ReaderQuotas; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + value.CopyTo(_encoding.ReaderQuotas); + } + } + + public NetMsmqSecurity Security + { + get { return _security; } + set { _security = value; } + } + + public EnvelopeVersion EnvelopeVersion => EnvelopeVersion.Soap12; + + public long MaxBufferPoolSize + { + get { return transport.MaxBufferPoolSize; } + set { transport.MaxBufferPoolSize = value; } + } + + internal int MaxPoolSize + { + get { return ((MsmqTransportBindingElement)transport).MaxPoolSize; } + set { ((MsmqTransportBindingElement)transport).MaxPoolSize = value; } + } + + [DefaultValue(MsmqDefaults.UseActiveDirectory)] + public bool UseActiveDirectory + { + get { return ((MsmqTransportBindingElement)transport).UseActiveDirectory; } + set { ((MsmqTransportBindingElement)transport).UseActiveDirectory = value; } + } + + // Default XmlDictionaryReaderQuotas (mirrors System.Xml's EncoderDefaults). + private const int DefaultMaxArrayLength = 16384; + private const int DefaultMaxBytesPerRead = 4096; + private const int DefaultMaxDepth = 32; + private const int DefaultMaxNameTableCharCount = 16384; + private const int DefaultMaxStringContentLength = 8192; + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeReaderQuotas() + { + return ReaderQuotas.MaxArrayLength != DefaultMaxArrayLength + || ReaderQuotas.MaxBytesPerRead != DefaultMaxBytesPerRead + || ReaderQuotas.MaxDepth != DefaultMaxDepth + || ReaderQuotas.MaxNameTableCharCount != DefaultMaxNameTableCharCount + || ReaderQuotas.MaxStringContentLength != DefaultMaxStringContentLength; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeSecurity() + { + if (_security.Mode != NetMsmqSecurity.DefaultMode) + { + return true; + } + if (_security.Transport.MsmqAuthenticationMode != MsmqDefaults.MsmqAuthenticationMode + || _security.Transport.MsmqEncryptionAlgorithm != MsmqDefaults.MsmqEncryptionAlgorithm + || _security.Transport.MsmqSecureHashAlgorithm != MsmqDefaults.MsmqSecureHashAlgorithm + || _security.Transport.MsmqProtectionLevel != MsmqDefaults.MsmqProtectionLevel) + { + return true; + } + if (_security.Message.AlgorithmSuite != MsmqDefaults.MessageSecurityAlgorithmSuite + || _security.Message.ClientCredentialType != MsmqDefaults.DefaultClientCredentialType) + { + return true; + } + return false; + } + + private void Initialize() + { + transport = new MsmqTransportBindingElement(); + _encoding = new BinaryMessageEncodingBindingElement(); + } + + public override BindingElementCollection CreateBindingElements() + { + var bindingElements = new BindingElementCollection(); + // Order matters: security (if any) -> encoding -> transport. + // Message security is not yet supported on the client port; see + // MessageSecurityOverMsmq for context. + bindingElements.Add(_encoding); + bindingElements.Add(GetTransport()); + return bindingElements.Clone(); + } + + private MsmqBindingElementBase GetTransport() + { + _security.ConfigureTransportSecurity(transport); + return transport; + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqSecurity.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqSecurity.cs new file mode 100644 index 00000000000..37fbb3098ae --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqSecurity.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.ComponentModel; +using System.ServiceModel.Channels; + +namespace System.ServiceModel +{ + public sealed class NetMsmqSecurity + { + internal const NetMsmqSecurityMode DefaultMode = NetMsmqSecurityMode.Transport; + + private NetMsmqSecurityMode _mode; + private MsmqTransportSecurity _transportSecurity; + private MessageSecurityOverMsmq _messageSecurity; + + public NetMsmqSecurity() + : this(DefaultMode, null, null) + { + } + + internal NetMsmqSecurity(NetMsmqSecurityMode mode) + : this(mode, null, null) + { + } + + private NetMsmqSecurity(NetMsmqSecurityMode mode, MsmqTransportSecurity transportSecurity, MessageSecurityOverMsmq messageSecurity) + { + if (!NetMsmqSecurityModeHelper.IsDefined(mode)) + { + throw new ArgumentOutOfRangeException(nameof(mode)); + } + _mode = mode; + _transportSecurity = transportSecurity ?? new MsmqTransportSecurity(); + _messageSecurity = messageSecurity ?? new MessageSecurityOverMsmq(); + } + + [DefaultValue(DefaultMode)] + public NetMsmqSecurityMode Mode + { + get { return _mode; } + set + { + if (!NetMsmqSecurityModeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _mode = value; + } + } + + public MsmqTransportSecurity Transport + { + get + { + return _transportSecurity ??= new MsmqTransportSecurity(); + } + set { _transportSecurity = value; } + } + + public MessageSecurityOverMsmq Message + { + get + { + return _messageSecurity ??= new MessageSecurityOverMsmq(); + } + set { _messageSecurity = value; } + } + + internal void ConfigureTransportSecurity(MsmqBindingElementBase msmq) + { + if (_mode == NetMsmqSecurityMode.Transport || _mode == NetMsmqSecurityMode.Both) + { + msmq.MsmqTransportSecurity = Transport; + } + else + { + msmq.MsmqTransportSecurity.Disable(); + } + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqSecurityMode.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqSecurityMode.cs new file mode 100644 index 00000000000..917e63ed6bb --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/NetMsmqSecurityMode.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel +{ + public enum NetMsmqSecurityMode + { + None, + Transport, + Message, + Both + } + + internal static class NetMsmqSecurityModeHelper + { + internal static bool IsDefined(NetMsmqSecurityMode value) + { + return (value == NetMsmqSecurityMode.Transport + || value == NetMsmqSecurityMode.Message + || value == NetMsmqSecurityMode.Both + || value == NetMsmqSecurityMode.None); + } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/PoisonMessageException.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/PoisonMessageException.cs new file mode 100644 index 00000000000..c2b205ca0bc --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/PoisonMessageException.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Serialization; + +namespace System.ServiceModel +{ + [Serializable] + public class PoisonMessageException : CommunicationException + { + public PoisonMessageException() { } + public PoisonMessageException(string message) : base(message) { } + public PoisonMessageException(string message, Exception innerException) : base(message, innerException) { } + protected PoisonMessageException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/src/System.ServiceModel.Msmq/src/System/ServiceModel/QueueTransferProtocol.cs b/src/System.ServiceModel.Msmq/src/System/ServiceModel/QueueTransferProtocol.cs new file mode 100644 index 00000000000..ab3bdf673cd --- /dev/null +++ b/src/System.ServiceModel.Msmq/src/System/ServiceModel/QueueTransferProtocol.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace System.ServiceModel +{ + public enum QueueTransferProtocol + { + Native, + Srmp, + SrmpSecure + } + + internal static class QueueTransferProtocolHelper + { + internal static bool IsDefined(QueueTransferProtocol value) + { + return value == QueueTransferProtocol.Native + || value == QueueTransferProtocol.Srmp + || value == QueueTransferProtocol.SrmpSecure; + } + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqBindingTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqBindingTest.cs new file mode 100644 index 00000000000..29da34bf0fd --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqBindingTest.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ComponentModel; +using System.Net.Security; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using Infrastructure.Common; +using Xunit; + +[SupportedOSPlatform("windows")] +public static class MsmqBindingTest +{ + [WcfFact] + public static void MsmqTransportBindingElement_Defaults() + { + var bel = new MsmqTransportBindingElement(); + Assert.Equal("net.msmq", bel.Scheme); + Assert.Equal(MsmqAuthenticationMode.WindowsDomain, bel.MsmqTransportSecurity.MsmqAuthenticationMode); + Assert.True(bel.Durable); + Assert.True(bel.ExactlyOnce); + Assert.Equal(DeadLetterQueue.System, bel.DeadLetterQueue); + Assert.Equal(QueueTransferProtocol.Native, bel.QueueTransferProtocol); + Assert.False(bel.UseActiveDirectory); + Assert.False(bel.UseMsmqTracing); + Assert.False(bel.UseSourceJournal); + } + + [WcfFact] + public static void MsmqTransportBindingElement_Clone_DeepCopy() + { + var original = new MsmqTransportBindingElement + { + Durable = false, + ExactlyOnce = false, + DeadLetterQueue = DeadLetterQueue.Custom, + CustomDeadLetterQueue = new Uri("net.msmq://localhost/private/dlq"), + QueueTransferProtocol = QueueTransferProtocol.Srmp, + UseActiveDirectory = true, + TimeToLive = TimeSpan.FromMinutes(30), + }; + original.MsmqTransportSecurity.MsmqAuthenticationMode = MsmqAuthenticationMode.Certificate; + + var clone = (MsmqTransportBindingElement)original.Clone(); + Assert.NotSame(original, clone); + Assert.NotSame(original.MsmqTransportSecurity, clone.MsmqTransportSecurity); + Assert.False(clone.Durable); + Assert.False(clone.ExactlyOnce); + Assert.Equal(DeadLetterQueue.Custom, clone.DeadLetterQueue); + Assert.Equal(original.CustomDeadLetterQueue, clone.CustomDeadLetterQueue); + Assert.Equal(QueueTransferProtocol.Srmp, clone.QueueTransferProtocol); + Assert.True(clone.UseActiveDirectory); + Assert.Equal(TimeSpan.FromMinutes(30), clone.TimeToLive); + Assert.Equal(MsmqAuthenticationMode.Certificate, clone.MsmqTransportSecurity.MsmqAuthenticationMode); + } + + [WcfFact] + public static void MsmqTransportBindingElement_CanBuildChannelFactory() + { + var bel = new MsmqTransportBindingElement(); + var binding = new CustomBinding(new BinaryMessageEncodingBindingElement(), bel); + var context = new BindingContext(binding, new BindingParameterCollection()); + Assert.True(bel.CanBuildChannelFactory(context)); + Assert.True(bel.CanBuildChannelFactory(context)); + Assert.False(bel.CanBuildChannelFactory(context)); + Assert.False(bel.CanBuildChannelFactory(context)); + } + + [WcfFact] + public static void MsmqTransportBindingElement_BuildChannelFactory_ReturnsFactory() + { + var bel = new MsmqTransportBindingElement(); + var binding = new CustomBinding(new BinaryMessageEncodingBindingElement(), bel); + var context = new BindingContext(binding, new BindingParameterCollection()); + IChannelFactory factory = bel.BuildChannelFactory(context); + Assert.NotNull(factory); + } + + [WcfFact] + public static void MsmqTransportBindingElement_BuildChannelFactory_UnsupportedType_Throws() + { + var bel = new MsmqTransportBindingElement(); + var binding = new CustomBinding(new BinaryMessageEncodingBindingElement(), bel); + var context = new BindingContext(binding, new BindingParameterCollection()); + Assert.Throws(() => bel.BuildChannelFactory(context)); + } + + [WcfTheory] + [InlineData(-1)] + public static void MsmqTransportBindingElement_NegativeMaxPoolSize_Throws(int bad) + { + var bel = new MsmqTransportBindingElement(); + Assert.Throws(() => bel.MaxPoolSize = bad); + } + + [WcfFact] + public static void MsmqTransportBindingElement_NegativeTimeToLive_Throws() + { + var bel = new MsmqTransportBindingElement(); + Assert.Throws(() => bel.TimeToLive = TimeSpan.FromSeconds(-1)); + } + + [WcfFact] + public static void NetMsmqBinding_Defaults() + { + var b = new NetMsmqBinding(); + Assert.Equal("net.msmq", b.Scheme); + Assert.Equal(EnvelopeVersion.Soap12, b.EnvelopeVersion); + Assert.Equal(NetMsmqSecurityMode.Transport, b.Security.Mode); + Assert.True(b.Durable); + Assert.True(b.ExactlyOnce); + Assert.Equal(QueueTransferProtocol.Native, b.QueueTransferProtocol); + Assert.False(b.UseActiveDirectory); + } + + [WcfTheory] + [InlineData(NetMsmqSecurityMode.None)] + [InlineData(NetMsmqSecurityMode.Transport)] + [InlineData(NetMsmqSecurityMode.Message)] + [InlineData(NetMsmqSecurityMode.Both)] + public static void NetMsmqBinding_SecurityModeCtor(NetMsmqSecurityMode mode) + { + var b = new NetMsmqBinding(mode); + Assert.Equal(mode, b.Security.Mode); + } + + [WcfFact] + public static void NetMsmqBinding_InvalidSecurityMode_Throws() + { + Assert.Throws(() => new NetMsmqBinding((NetMsmqSecurityMode)42)); + } + + [WcfFact] + public static void NetMsmqBinding_CreateBindingElements_HasEncodingAndTransport() + { + var b = new NetMsmqBinding(); + var elements = b.CreateBindingElements(); + Assert.Contains(elements, e => e is BinaryMessageEncodingBindingElement); + Assert.Contains(elements, e => e is MsmqTransportBindingElement); + Assert.Equal(2, elements.Count); + } + + [WcfFact] + public static void NetMsmqBinding_TransportSecurityModeAppliesToTransportElement() + { + var b = new NetMsmqBinding(NetMsmqSecurityMode.None); + var elements = b.CreateBindingElements(); + var transport = (MsmqTransportBindingElement)elements.Find(); + // SecurityMode.None disables transport security + Assert.Equal(MsmqAuthenticationMode.None, transport.MsmqTransportSecurity.MsmqAuthenticationMode); + Assert.Equal(ProtectionLevel.None, transport.MsmqTransportSecurity.MsmqProtectionLevel); + } + + [WcfFact] + public static void NetMsmqBinding_TransportMode_KeepsTransportSecurityEnabled() + { + var b = new NetMsmqBinding(NetMsmqSecurityMode.Transport); + var elements = b.CreateBindingElements(); + var transport = (MsmqTransportBindingElement)elements.Find(); + Assert.Equal(MsmqAuthenticationMode.WindowsDomain, transport.MsmqTransportSecurity.MsmqAuthenticationMode); + Assert.Equal(ProtectionLevel.Sign, transport.MsmqTransportSecurity.MsmqProtectionLevel); + } + + [WcfFact] + public static void NetMsmqBinding_ReaderQuotas_NullThrows() + { + var b = new NetMsmqBinding(); + Assert.Throws(() => b.ReaderQuotas = null); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqConditionsTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqConditionsTest.cs new file mode 100644 index 00000000000..146520580e0 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqConditionsTest.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Reflection; +using System.Runtime.Versioning; +using Infrastructure.Common; +using Xunit; + +// Regression tests for the slice-5c bug: +// .NET 8+ disables implicit DTC promotion by default. Setting +// TransactionManager.ImplicitDistributedTransactions from inside the +// xunit-console test host is too late: the DTC proxy reads the flag +// on first use, which xunit's discovery layer triggers before our +// test code runs. The MSMQ transactional scenarios therefore have to +// be gated by an environment variable that the dev / CI sets only +// when the runtime has the flag in place from the start. +// +// These tests pin the env-var contract so a future refactor of +// ConditionalTestDetectors doesn't silently re-enable the failing +// scenarios on every machine. +[SupportedOSPlatform("windows")] +public static class MsmqConditionsTest +{ + private const string EnvVar = "WCF_MSMQ_ENABLE_DTC_TESTS"; + + private static readonly Type s_detectors = + typeof(ConditionalWcfTest).Assembly + .GetType("Infrastructure.Common.ConditionalTestDetectors", throwOnError: true); + + private static bool InvokeDetector(string name) + { + MethodInfo m = s_detectors.GetMethod(name, + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(m); + return (bool)m.Invoke(null, null); + } + + [WcfFact] + public static void ImplicitDtcEnabled_DefaultsToFalse() + { + string previous = Environment.GetEnvironmentVariable(EnvVar); + Environment.SetEnvironmentVariable(EnvVar, null); + try + { + Assert.False(InvokeDetector("IsImplicitDtcEnabled")); + } + finally + { + Environment.SetEnvironmentVariable(EnvVar, previous); + } + } + + [WcfTheory] + [InlineData("true")] + [InlineData("TRUE")] + [InlineData("True")] + public static void ImplicitDtcEnabled_HonorsEnvVar(string envValue) + { + string previous = Environment.GetEnvironmentVariable(EnvVar); + Environment.SetEnvironmentVariable(EnvVar, envValue); + try + { + // Detector is Windows-only by design (MSMQ is Windows-only). + // On non-Windows hosts the env var is ignored and the + // detector always returns false. + bool expected = OperatingSystem.IsWindows(); + Assert.Equal(expected, InvokeDetector("IsImplicitDtcEnabled")); + } + finally + { + Environment.SetEnvironmentVariable(EnvVar, previous); + } + } + + [WcfTheory] + [InlineData("false")] + [InlineData("0")] + [InlineData("")] + [InlineData("anything-else")] + public static void ImplicitDtcEnabled_OnlyAcceptsTrueLiteral(string envValue) + { + string previous = Environment.GetEnvironmentVariable(EnvVar); + Environment.SetEnvironmentVariable(EnvVar, envValue); + try + { + Assert.False(InvokeDetector("IsImplicitDtcEnabled")); + } + finally + { + Environment.SetEnvironmentVariable(EnvVar, previous); + } + } + + // Regression: mqrt.dll presence is the gate for MSMQ scenario + // tests. Anything that swaps the check (e.g. a service-running + // check that misses non-running queue managers, or a registry probe + // that needs admin) would silently start failing on Helix workers + // that can't satisfy the new check. + [WcfFact] + public static void MsmqInstalled_ChecksMqrtDll() + { + bool detectorSaysInstalled = InvokeDetector("IsMsmqInstalled"); + string mqrt = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "mqrt.dll"); + Assert.Equal(System.IO.File.Exists(mqrt), detectorSaysInstalled); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqExceptionTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqExceptionTest.cs new file mode 100644 index 00000000000..1806577c13b --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqExceptionTest.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Reflection; +using System.ServiceModel; +using Infrastructure.Common; +using Xunit; + +public static class MsmqExceptionTest +{ + private static (bool faultSender, bool faultReceiver, Type normalizedType) MapErrorCode(uint code) + { + var ex = new MsmqException("test", unchecked((int)code)); + Type t = typeof(MsmqException); + bool faultSender = (bool)t.GetProperty("FaultSender", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ex); + bool faultReceiver = (bool)t.GetProperty("FaultReceiver", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ex); + Type outer = (Type)t.GetProperty("NormalizedType", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ex); + return (faultSender, faultReceiver, outer); + } + + [WcfTheory] + [InlineData(0xC00E0003u, typeof(EndpointNotFoundException))] // QueueNotFound + [InlineData(0xC00E001Eu, typeof(ArgumentException))] // IllegalFormatName + [InlineData(0xC00E0014u, typeof(ArgumentException))] // IllegalQueuePathName + [InlineData(0xC00E001Bu, typeof(TimeoutException))] // IOTimeout + [InlineData(0xC00E0026u, typeof(EndpointNotFoundException))] // QueueNotAvailable + [InlineData(0xC00E000Eu, typeof(EndpointNotFoundException))] // RemoteMachineNotAvailable + [InlineData(0xC00E000Bu, typeof(EndpointNotFoundException))] // ServiceNotAvailable + [InlineData(0xC00E0050u, typeof(InvalidOperationException))] // TransactionUsage + [InlineData(0xC00E0006u, typeof(InvalidOperationException))] // StaleHandle + [InlineData(0xC00E0025u, typeof(AddressAccessDeniedException))] // AccessDenied — now lives in Primitives (slice 10) + [InlineData(0xC00E0009u, typeof(AddressAccessDeniedException))] // SharingViolation + [InlineData(0xC00E0027u, typeof(CommunicationException))] // InsufficientResources + public static void Normalized_MapsErrorCodeToWcfExceptionType(uint code, Type expected) + { + var (_, _, outer) = MapErrorCode(code); + Assert.Equal(expected, outer); + } + + [WcfFact] + public static void Normalized_UnknownErrorCode_ReturnsSelf() + { + var ex = new MsmqException("test", 0x12345678); + Type t = typeof(MsmqException); + Type outer = (Type)t.GetProperty("NormalizedType", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ex); + Assert.Null(outer); + Exception normalized = (Exception)t.GetProperty("Normalized", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ex); + Assert.Same(ex, normalized); + } + + [WcfTheory] + [InlineData(0xC00E001Bu, false, false)] // IOTimeout: neither faulted + [InlineData(0xC00E001Eu, false, false)] // IllegalFormatName: neither + [InlineData(0xC00E0026u, false, true)] // QueueNotAvailable: receiver only + [InlineData(0xC00E0003u, true, true)] // QueueNotFound: both + public static void FaultSenderReceiver_MatchesNetFxTable(uint code, bool expectedSender, bool expectedReceiver) + { + var (sender, receiver, _) = MapErrorCode(code); + Assert.Equal(expectedSender, sender); + Assert.Equal(expectedReceiver, receiver); + } + + [WcfFact] + public static void MsmqException_Default_ErrorCodeIsExternalExceptionDefault() + { + // ExternalException() uses E_FAIL (0x80004005) as the default + // HResult on .NET (Core+). The default ctor is rarely useful for + // MsmqException since callers normally have an actual MSMQ error + // code; we just lock the current behavior so it doesn't change + // silently. + Assert.Equal(unchecked((int)0x80004005), new MsmqException().ErrorCode); + } + + [WcfFact] + public static void MsmqException_PreservesErrorCode() + { + var ex = new MsmqException("hi", unchecked((int)0xC00E0003u)); + Assert.Equal(unchecked((int)0xC00E0003u), ex.ErrorCode); + Assert.Equal("hi", ex.Message); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqIntegrationBindingTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqIntegrationBindingTest.cs new file mode 100644 index 00000000000..591078ced9d --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqIntegrationBindingTest.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ComponentModel; +using System.Net.Security; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.MsmqIntegration; +using Infrastructure.Common; +using Xunit; + +public static class MsmqIntegrationBindingTest +{ + [WcfFact] + public static void MsmqIntegrationBindingElement_Defaults() + { + var bel = new MsmqIntegrationBindingElement(); + Assert.Equal("msmq.formatname", bel.Scheme); + Assert.Equal(MsmqMessageSerializationFormat.Xml, bel.SerializationFormat); + Assert.Null(bel.TargetSerializationTypes); + } + + [WcfFact] + public static void MsmqIntegrationBindingElement_TargetTypes_RoundTrip_DefensiveCopy() + { + var bel = new MsmqIntegrationBindingElement(); + var input = new[] { typeof(int), typeof(string) }; + bel.TargetSerializationTypes = input; + + var roundTrip = bel.TargetSerializationTypes; + Assert.Equal(input, roundTrip); + Assert.NotSame(input, roundTrip); + + // Mutating the input array after assignment must not leak into the element. + input[0] = typeof(object); + Assert.Equal(typeof(int), bel.TargetSerializationTypes[0]); + } + + [WcfFact] + public static void MsmqIntegrationBindingElement_Clone_DeepCopy() + { + var original = new MsmqIntegrationBindingElement + { + SerializationFormat = MsmqMessageSerializationFormat.Binary, + TargetSerializationTypes = new[] { typeof(DateTime) }, + Durable = false, + }; + + var clone = (MsmqIntegrationBindingElement)original.Clone(); + Assert.Equal(MsmqMessageSerializationFormat.Binary, clone.SerializationFormat); + Assert.Equal(typeof(DateTime), clone.TargetSerializationTypes[0]); + Assert.False(clone.Durable); + Assert.NotSame(original.TargetSerializationTypes, clone.TargetSerializationTypes); + } + + [WcfFact] + public static void MsmqIntegrationBindingElement_InvalidFormat_Throws() + { + var bel = new MsmqIntegrationBindingElement(); + Assert.Throws(() => bel.SerializationFormat = (MsmqMessageSerializationFormat)42); + } + + [WcfFact] + public static void MsmqIntegrationBindingElement_CanBuildChannelFactory() + { + var bel = new MsmqIntegrationBindingElement(); + var binding = new CustomBinding(bel); + var context = new BindingContext(binding, new BindingParameterCollection()); + Assert.True(bel.CanBuildChannelFactory(context)); + Assert.False(bel.CanBuildChannelFactory(context)); + Assert.False(bel.CanBuildChannelFactory(context)); + } + + [WcfFact] + [SupportedOSPlatform("windows")] + public static void MsmqIntegrationBindingElement_BuildChannelFactory_ReturnsFactory() + { + var bel = new MsmqIntegrationBindingElement(); + var binding = new CustomBinding(bel); + var context = new BindingContext(binding, new BindingParameterCollection()); + IChannelFactory factory = bel.BuildChannelFactory(context); + Assert.NotNull(factory); + } + + [WcfFact] + public static void MsmqIntegrationBindingElement_GetProperty_MessageVersion() + { + var bel = new MsmqIntegrationBindingElement(); + var binding = new CustomBinding(bel); + var context = new BindingContext(binding, new BindingParameterCollection()); + Assert.Same(MessageVersion.None, bel.GetProperty(context)); + } + + [WcfFact] + public static void MsmqIntegrationBinding_Defaults() + { + var b = new MsmqIntegrationBinding(); + Assert.Equal("msmq.formatname", b.Scheme); + Assert.Equal(MsmqIntegrationSecurityMode.Transport, b.Security.Mode); + Assert.Equal(MsmqMessageSerializationFormat.Xml, b.SerializationFormat); + Assert.True(b.Durable); + Assert.True(b.ExactlyOnce); + } + + [WcfTheory] + [InlineData(MsmqIntegrationSecurityMode.None)] + [InlineData(MsmqIntegrationSecurityMode.Transport)] + public static void MsmqIntegrationBinding_SecurityModeCtor(MsmqIntegrationSecurityMode mode) + { + var b = new MsmqIntegrationBinding(mode); + Assert.Equal(mode, b.Security.Mode); + } + + [WcfFact] + public static void MsmqIntegrationBinding_InvalidSecurityMode_Throws() + { + Assert.Throws(() => new MsmqIntegrationBinding((MsmqIntegrationSecurityMode)42)); + } + + [WcfFact] + public static void MsmqIntegrationBinding_CreateBindingElements_TransportOnly() + { + var b = new MsmqIntegrationBinding(); + var elements = b.CreateBindingElements(); + Assert.Single(elements); + Assert.IsType(elements[0]); + } + + [WcfFact] + public static void MsmqIntegrationBinding_SecurityNone_DisablesTransport() + { + var b = new MsmqIntegrationBinding(MsmqIntegrationSecurityMode.None); + var elements = b.CreateBindingElements(); + var transport = (MsmqIntegrationBindingElement)elements.Find(); + Assert.Equal(MsmqAuthenticationMode.None, transport.MsmqTransportSecurity.MsmqAuthenticationMode); + Assert.Equal(ProtectionLevel.None, transport.MsmqTransportSecurity.MsmqProtectionLevel); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqIntegrationEnumValueTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqIntegrationEnumValueTest.cs new file mode 100644 index 00000000000..df2f1cf9046 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqIntegrationEnumValueTest.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Versioning; +using System.ServiceModel.MsmqIntegration; +using Infrastructure.Common; +using Xunit; + +// Pins the underlying integer value of every member of the +// MSMQ-Integration enums (AcknowledgeTypes, Acknowledgment, +// MessageType, MessagePriority). The values must match the native +// MQMSG_* / MQMSG_CLASS_* constants and the corresponding members +// in netfx System.Messaging exactly so that: +// +// * round-tripping a property through the MSMQ ABI loses no +// information, +// * source-compatible callers porting from netfx see the same +// numeric values they always saw, and +// * an inadvertent rename or renumber during a future refactor is +// caught at test time rather than at deployment time. +// +// Treat these as contract tests — only change them if you also +// intend to change the public enum surface, and only after +// confirming the native MSMQ constants haven't moved. +[SupportedOSPlatform("windows")] +public static class MsmqIntegrationEnumValueTest +{ + [WcfTheory] + [InlineData(AcknowledgeTypes.None, 0)] + [InlineData(AcknowledgeTypes.PositiveArrival, 1)] + [InlineData(AcknowledgeTypes.PositiveReceive, 2)] + [InlineData(AcknowledgeTypes.NegativeArrival, 4)] + [InlineData(AcknowledgeTypes.FullReachQueue, 5)] + [InlineData(AcknowledgeTypes.NegativeReceive, 8)] + [InlineData(AcknowledgeTypes.NotAcknowledgeReceive, 12)] + [InlineData(AcknowledgeTypes.FullReceive, 14)] + public static void AcknowledgeTypes_Values(AcknowledgeTypes value, int expected) + { + Assert.Equal(expected, (int)value); + } + + [WcfFact] + public static void AcknowledgeTypes_NotAcknowledgeReachQueue_IsAliasFor_NegativeArrival() + { + // Both members are defined by netfx with the same underlying + // value (4). Encoded as a fact rather than an InlineData pair + // because xunit's xUnit1025 analyzer rejects two theory rows + // that decay to the same boxed-int signature. + Assert.Equal((int)AcknowledgeTypes.NegativeArrival, (int)AcknowledgeTypes.NotAcknowledgeReachQueue); + Assert.Equal(4, (int)AcknowledgeTypes.NotAcknowledgeReachQueue); + } + + [WcfFact] + public static void AcknowledgeTypes_FlagComposition() + { + Assert.Equal( + AcknowledgeTypes.PositiveArrival | AcknowledgeTypes.NegativeArrival, + AcknowledgeTypes.FullReachQueue); + Assert.Equal( + AcknowledgeTypes.PositiveReceive | AcknowledgeTypes.NegativeReceive | AcknowledgeTypes.NegativeArrival, + AcknowledgeTypes.FullReceive); + Assert.Equal( + AcknowledgeTypes.NegativeArrival | AcknowledgeTypes.NegativeReceive, + AcknowledgeTypes.NotAcknowledgeReceive); + } + + [WcfFact] + public static void AcknowledgeTypes_HasFlagsAttribute() + { + Assert.NotNull(typeof(AcknowledgeTypes).GetCustomAttributes(typeof(FlagsAttribute), inherit: false)); + Assert.NotEmpty(typeof(AcknowledgeTypes).GetCustomAttributes(typeof(FlagsAttribute), inherit: false)); + } + + [WcfTheory] + [InlineData(Acknowledgment.None, 0x0000)] + [InlineData(Acknowledgment.ReachQueue, 0x0002)] + [InlineData(Acknowledgment.Receive, 0x4000)] + [InlineData(Acknowledgment.QueuePurged, 0x4001)] + [InlineData(Acknowledgment.ReceiveTimeout, 0x4002)] + [InlineData(Acknowledgment.ReachQueueTimeout, 0x4003)] + [InlineData(Acknowledgment.ReceiveRejected, 0x4005)] + [InlineData(Acknowledgment.BadDestinationQueue, 0x8000)] + [InlineData(Acknowledgment.Purged, 0x8001)] + [InlineData(Acknowledgment.QueueExceedMaximumSize, 0x8002)] + [InlineData(Acknowledgment.AccessDenied, 0x8004)] + [InlineData(Acknowledgment.HopCountExceeded, 0x8005)] + [InlineData(Acknowledgment.BadSignature, 0x8006)] + [InlineData(Acknowledgment.BadEncryption, 0x8007)] + [InlineData(Acknowledgment.CouldNotEncrypt, 0x8008)] + [InlineData(Acknowledgment.NotTransactionalQueue, 0x8009)] + [InlineData(Acknowledgment.NotTransactionalMessage, 0x800A)] + public static void Acknowledgment_Values(Acknowledgment value, int expected) + { + Assert.Equal(expected, (int)value); + } + + [WcfTheory] + [InlineData(MessageType.Normal, 1)] + [InlineData(MessageType.Response, 2)] + [InlineData(MessageType.Report, 3)] + [InlineData(MessageType.Acknowledgment, 6)] + public static void MessageType_Values(MessageType value, int expected) + { + Assert.Equal(expected, (int)value); + } + + [WcfTheory] + [InlineData(MessagePriority.Lowest, 0)] + [InlineData(MessagePriority.VeryLow, 1)] + [InlineData(MessagePriority.Low, 2)] + [InlineData(MessagePriority.Normal, 3)] + [InlineData(MessagePriority.AboveNormal, 4)] + [InlineData(MessagePriority.High, 5)] + [InlineData(MessagePriority.VeryHigh, 6)] + [InlineData(MessagePriority.Highest, 7)] + public static void MessagePriority_Values(MessagePriority value, int expected) + { + Assert.Equal(expected, (int)value); + } + + // CLS compliance: ours-only enums (no longer flowing through + // MSMQ.Messaging's [CLSCompliant(false)] surface) means the + // MsmqMessage / MsmqIntegrationMessageProperty members that + // expose them no longer need member-level CLS exclusions. + [WcfTheory] + [InlineData(nameof(MsmqMessage.AcknowledgeType))] + [InlineData(nameof(MsmqMessage.Acknowledgment))] + [InlineData(nameof(MsmqMessage.MessageType))] + [InlineData(nameof(MsmqMessage.Priority))] + public static void MsmqMessageT_EnumMembers_NoCLSCompliantFalseAttribute(string memberName) + { + var prop = typeof(MsmqMessage).GetProperty(memberName); + Assert.NotNull(prop); + var attrs = prop.GetCustomAttributes(typeof(CLSCompliantAttribute), inherit: false); + Assert.Empty(attrs); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqIntegrationOutputChannelFactoryTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqIntegrationOutputChannelFactoryTest.cs new file mode 100644 index 00000000000..34bf4cdeec7 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqIntegrationOutputChannelFactoryTest.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.MsmqIntegration; +using Infrastructure.Common; +using Xunit; + +[SupportedOSPlatform("windows")] +public static class MsmqIntegrationOutputChannelFactoryTest +{ + private static IChannelFactory BuildFactory() + { + var binding = new MsmqIntegrationBinding(MsmqIntegrationSecurityMode.None) + { + ExactlyOnce = false, + Durable = false, + }; + return binding.BuildChannelFactory(); + } + + [WcfFact] + public static void MsmqIntegrationBinding_BuildChannelFactory_Succeeds() + { + IChannelFactory factory = BuildFactory(); + Assert.NotNull(factory); + factory.Close(); + } + + [WcfFact] + public static void Factory_OpenClose_Lifecycle() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + Assert.Equal(CommunicationState.Opened, factory.State); + factory.Close(); + Assert.Equal(CommunicationState.Closed, factory.State); + } + + [WcfFact] + public static void Factory_CreateChannel_PreservesAddresses() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + var ea = new EndpointAddress("msmq.formatname:DIRECT=OS:.\\private$\\q1"); + IOutputChannel channel = factory.CreateChannel(ea); + try + { + Assert.Same(ea, channel.RemoteAddress); + Assert.Equal(ea.Uri, channel.Via); + } + finally + { + channel.Abort(); + } + } + + [WcfFact] + public static void Factory_GetProperty_MessageVersion_ReturnsNone() + { + IChannelFactory factory = BuildFactory(); + Assert.Same(MessageVersion.None, factory.GetProperty()); + } + + [WcfFact] + public static void Factory_RejectsUnsupportedChannelType() + { + var binding = new MsmqIntegrationBinding(MsmqIntegrationSecurityMode.None); + Assert.Throws(() => binding.BuildChannelFactory()); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqLeafTypesTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqLeafTypesTest.cs new file mode 100644 index 00000000000..8dd01899cfc --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqLeafTypesTest.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ServiceModel; +using System.ServiceModel.MsmqIntegration; +using Infrastructure.Common; +using Xunit; + +public static class MsmqLeafTypesTest +{ + [WcfTheory] + [InlineData(MsmqAuthenticationMode.None, true)] + [InlineData(MsmqAuthenticationMode.WindowsDomain, true)] + [InlineData(MsmqAuthenticationMode.Certificate, true)] + [InlineData((MsmqAuthenticationMode)42, false)] + public static void MsmqAuthenticationModeHelper_IsDefined(MsmqAuthenticationMode mode, bool expected) + { + Type t = typeof(MsmqAuthenticationMode).Assembly.GetType("System.ServiceModel.MsmqAuthenticationModeHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined").Invoke(null, new object[] { mode }); + Assert.Equal(expected, actual); + } + + [WcfTheory] + [InlineData(MsmqEncryptionAlgorithm.RC4Stream, true)] + [InlineData(MsmqEncryptionAlgorithm.Aes, true)] + [InlineData((MsmqEncryptionAlgorithm)42, false)] + public static void MsmqEncryptionAlgorithmHelper_IsDefined(MsmqEncryptionAlgorithm value, bool expected) + { + Type t = typeof(MsmqEncryptionAlgorithm).Assembly.GetType("System.ServiceModel.MsmqEncryptionAlgorithmHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined").Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } + + [WcfTheory] + [InlineData(MsmqSecureHashAlgorithm.MD5, true)] + [InlineData(MsmqSecureHashAlgorithm.Sha1, true)] + [InlineData(MsmqSecureHashAlgorithm.Sha256, true)] + [InlineData(MsmqSecureHashAlgorithm.Sha512, true)] + [InlineData((MsmqSecureHashAlgorithm)42, false)] + public static void MsmqSecureHashAlgorithmHelper_IsDefined(MsmqSecureHashAlgorithm value, bool expected) + { + Type t = typeof(MsmqSecureHashAlgorithm).Assembly.GetType("System.ServiceModel.MsmqSecureHashAlgorithmHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined").Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } + + [WcfTheory] + [InlineData(NetMsmqSecurityMode.None, true)] + [InlineData(NetMsmqSecurityMode.Transport, true)] + [InlineData(NetMsmqSecurityMode.Message, true)] + [InlineData(NetMsmqSecurityMode.Both, true)] + [InlineData((NetMsmqSecurityMode)42, false)] + public static void NetMsmqSecurityModeHelper_IsDefined(NetMsmqSecurityMode value, bool expected) + { + Type t = typeof(NetMsmqSecurityMode).Assembly.GetType("System.ServiceModel.NetMsmqSecurityModeHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic).Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } + + [WcfTheory] + [InlineData(MsmqIntegrationSecurityMode.None, true)] + [InlineData(MsmqIntegrationSecurityMode.Transport, true)] + [InlineData((MsmqIntegrationSecurityMode)42, false)] + public static void MsmqIntegrationSecurityModeHelper_IsDefined(MsmqIntegrationSecurityMode value, bool expected) + { + Type t = typeof(MsmqIntegrationSecurityMode).Assembly.GetType("System.ServiceModel.MsmqIntegration.MsmqIntegrationSecurityModeHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic).Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } + + [WcfTheory] + [InlineData(MsmqMessageSerializationFormat.Xml, true)] + [InlineData(MsmqMessageSerializationFormat.Binary, true)] + [InlineData(MsmqMessageSerializationFormat.ActiveX, true)] + [InlineData(MsmqMessageSerializationFormat.ByteArray, true)] + [InlineData(MsmqMessageSerializationFormat.Stream, true)] + [InlineData((MsmqMessageSerializationFormat)42, false)] + public static void MsmqMessageSerializationFormatHelper_IsDefined(MsmqMessageSerializationFormat value, bool expected) + { + Type t = typeof(MsmqMessageSerializationFormat).Assembly.GetType("System.ServiceModel.MsmqIntegration.MsmqMessageSerializationFormatHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic).Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } + + [WcfFact] + public static void PoisonMessageException_IsCommunicationException() + { + var ex = new PoisonMessageException("msg"); + Assert.IsAssignableFrom(ex); + Assert.Equal("msg", ex.Message); + } + + [WcfFact] + public static void MsmqPoisonMessageException_DefaultLookupId_IsZero() + { + var ex = new MsmqPoisonMessageException(); + Assert.Equal(0L, ex.MessageLookupId); + } + + [WcfFact] + public static void MsmqPoisonMessageException_LookupIdCtor_RoundTrips() + { + var inner = new InvalidOperationException("inner"); + var ex = new MsmqPoisonMessageException(123456789L, inner); + Assert.Equal(123456789L, ex.MessageLookupId); + Assert.Same(inner, ex.InnerException); + Assert.IsAssignableFrom(ex); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqMessageTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqMessageTest.cs new file mode 100644 index 00000000000..689dd3e890e --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqMessageTest.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Versioning; +using System.ServiceModel.MsmqIntegration; +using Infrastructure.Common; +using Xunit; + +[SupportedOSPlatform("windows")] +public static class MsmqMessageTest +{ + [WcfFact] + public static void MsmqMessage_Ctor_NullBody_Throws() + { + Assert.Throws(() => new MsmqMessage(null)); + } + + [WcfFact] + public static void MsmqMessage_BodyAndProperties_RoundTrip() + { + var m = new MsmqMessage("hello") + { + Label = "demo-label", + CorrelationId = "abcd", + AppSpecific = 42, + Priority = MessagePriority.High, + AcknowledgeType = AcknowledgeTypes.FullReceive, + TimeToReachQueue = TimeSpan.FromMinutes(5), + ResponseQueue = new Uri("net.msmq://localhost/private/response"), + }; + + Assert.Equal("hello", m.Body); + Assert.Equal("demo-label", m.Label); + Assert.Equal("abcd", m.CorrelationId); + Assert.Equal(42, m.AppSpecific); + Assert.Equal(MessagePriority.High, m.Priority); + Assert.Equal(AcknowledgeTypes.FullReceive, m.AcknowledgeType); + Assert.Equal(TimeSpan.FromMinutes(5), m.TimeToReachQueue); + Assert.Equal(new Uri("net.msmq://localhost/private/response"), m.ResponseQueue); + } + + [WcfFact] + public static void MsmqMessage_BodySetterRejectsNull() + { + var m = new MsmqMessage("a"); + Assert.Throws(() => m.Body = null); + } + + [WcfFact] + public static void MsmqMessage_ReadOnlyProperties_AreInternalOnly() + { + // These mirror MSMQ's "set by transport" fields — should not have a + // public setter on MsmqMessage. + Type t = typeof(MsmqMessage); + foreach (string p in new[] { "Acknowledgment", "ArrivedTime", "Authenticated", "DestinationQueue", "Id", "MessageType", "SenderId", "SentTime" }) + { + var prop = t.GetProperty(p); + Assert.NotNull(prop); + Assert.Null(prop.SetMethod); + } + } + + [WcfFact] + public static void MsmqIntegrationMessageProperty_NameConstant() + { + Assert.Equal("MsmqIntegrationMessageProperty", MsmqIntegrationMessageProperty.Name); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqMessagingInteropTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqMessagingInteropTest.cs new file mode 100644 index 00000000000..9dea921b32b --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqMessagingInteropTest.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Reflection; +using System.Runtime.Versioning; +using System.ServiceModel; +using Infrastructure.Common; +using Xunit; + +// Pins the native MQ_ERROR_* → WCF-exception mapping after slice 9 +// dropped the MSMQ.Messaging dependency. The native send path now +// passes the raw HRESULT from mqrt!MQSendMessage straight into +// MsmqException's (string, int) ctor; previously the wrapper layer +// went through MSMQ.Messaging.MessageQueueException whose .ErrorCode +// is the generic HRESULT (0x80004005). Slice 6 had to undo that with +// MessageQueueErrorCode; slice 9 eliminates the wrapper entirely so +// we no longer have a substitute layer to test through. +// +// These tests construct MsmqException directly (the public ctor) with +// the native MQ_ERROR_* code and verify the normalization table fires +// correctly. If a future refactor reintroduces a wrapper layer, the +// new wrapper must preserve the native code or these assertions will +// detect the regression. +[SupportedOSPlatform("windows")] +public static class MsmqMessagingInteropTest +{ + private static Type s_normalizedTypeProperty; + + private static Type GetNormalizedType(MsmqException ex) + { + if (s_normalizedTypeProperty == null) + { + s_normalizedTypeProperty = typeof(MsmqException); + } + PropertyInfo p = s_normalizedTypeProperty.GetProperty("NormalizedType", + BindingFlags.Instance | BindingFlags.NonPublic); + return (Type)p.GetValue(ex); + } + + // Sanity check: the native MQ_ERROR_* code passed to MsmqException + // round-trips through ErrorCode without bit munging. This is the + // critical invariant — if it ever changes, every normalized + // exception type silently becomes a bare MsmqException because the + // TuneBehavior switch in MsmqException keys off the value. + [WcfTheory] + [InlineData(0xC00E0003u)] // MQ_ERROR_QUEUE_NOT_FOUND + [InlineData(0xC00E001Bu)] // MQ_ERROR_IO_TIMEOUT + [InlineData(0xC00E001Eu)] // MQ_ERROR_ILLEGAL_FORMATNAME + [InlineData(0xC00E0025u)] // MQ_ERROR_ACCESS_DENIED + public static void MsmqException_PreservesNativeMQErrorCode(uint nativeCode) + { + var ex = new MsmqException("native send failed", unchecked((int)nativeCode)); + Assert.Equal(unchecked((int)nativeCode), ex.ErrorCode); + } + + // End-to-end pin of the post-slice-9 send-error path: passing the + // native MQ_ERROR_* code from mqrt!MQSendMessage's return value + // into MsmqException, then asking MsmqException.NormalizedType, + // yields the right WCF exception type. + [WcfTheory] + [InlineData(0xC00E0003u, typeof(EndpointNotFoundException))] // QueueNotFound + [InlineData(0xC00E001Bu, typeof(TimeoutException))] // IOTimeout + [InlineData(0xC00E001Eu, typeof(ArgumentException))] // IllegalFormatName + [InlineData(0xC00E0025u, typeof(AddressAccessDeniedException))] // AccessDenied + [InlineData(0xC00E000Eu, typeof(EndpointNotFoundException))] // RemoteMachineNotAvailable + public static void MsmqException_NormalizesToWcfException(uint nativeCode, Type expected) + { + var ex = new MsmqException("native send failed", unchecked((int)nativeCode)); + Assert.Equal(expected, GetNormalizedType(ex)); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqOutputChannelFactoryTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqOutputChannelFactoryTest.cs new file mode 100644 index 00000000000..aa161ee560c --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqOutputChannelFactoryTest.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using Infrastructure.Common; +using Xunit; + +[SupportedOSPlatform("windows")] +public static class MsmqOutputChannelFactoryTest +{ + private static IChannelFactory BuildFactory() + { + var binding = new NetMsmqBinding(NetMsmqSecurityMode.None) { ExactlyOnce = false, Durable = false }; + return binding.BuildChannelFactory(); + } + + [WcfFact] + public static void NetMsmqBinding_BuildChannelFactory_Succeeds() + { + IChannelFactory factory = BuildFactory(); + Assert.NotNull(factory); + } + + [WcfFact] + public static void Factory_OpenClose_Lifecycle() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + Assert.Equal(CommunicationState.Opened, factory.State); + factory.Close(); + Assert.Equal(CommunicationState.Closed, factory.State); + } + + [WcfFact] + public static void Factory_CreateChannel_ValidatesAddress() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + Assert.Throws(() => factory.CreateChannel(null)); + } + + [WcfFact] + public static void Factory_CreateChannel_ReturnsChannelWithExpectedAddresses() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + var ea = new EndpointAddress("net.msmq://localhost/private/scratch"); + IOutputChannel channel = factory.CreateChannel(ea); + try + { + Assert.Same(ea, channel.RemoteAddress); + Assert.Equal(new Uri("net.msmq://localhost/private/scratch"), channel.Via); + } + finally + { + channel.Abort(); + } + } + + [WcfFact] + public static void Factory_CreateChannel_RejectsUnsupportedScheme() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + var ea = new EndpointAddress("http://localhost/svc"); + Assert.Throws(() => factory.CreateChannel(ea)); + } + + [WcfFact] + public static void Factory_GetProperty_MessageVersion_ReturnsSoap12() + { + IChannelFactory factory = BuildFactory(); + MessageVersion mv = factory.GetProperty(); + Assert.Equal(MessageVersion.Soap12WSAddressing10, mv); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqOutputSessionChannelFactoryTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqOutputSessionChannelFactoryTest.cs new file mode 100644 index 00000000000..afbc1aee250 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqOutputSessionChannelFactoryTest.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Versioning; +using System.ServiceModel; +using System.ServiceModel.Channels; +using Infrastructure.Common; +using Xunit; + +[SupportedOSPlatform("windows")] +public static class MsmqOutputSessionChannelFactoryTest +{ + private static IChannelFactory BuildFactory() + { + var binding = new NetMsmqBinding(NetMsmqSecurityMode.None) { ExactlyOnce = false, Durable = false }; + return binding.BuildChannelFactory(); + } + + [WcfFact] + public static void NetMsmqBinding_BuildSessionChannelFactory_Succeeds() + { + IChannelFactory factory = BuildFactory(); + Assert.NotNull(factory); + factory.Close(); + } + + [WcfFact] + public static void Factory_CreateChannel_HasUniqueSessionId() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + var ea = new EndpointAddress("net.msmq://localhost/private/scratch"); + IOutputSessionChannel c1 = factory.CreateChannel(ea); + IOutputSessionChannel c2 = factory.CreateChannel(ea); + try + { + Assert.NotNull(c1.Session); + Assert.NotNull(c1.Session.Id); + Assert.NotEmpty(c1.Session.Id); + Assert.NotEqual(c1.Session.Id, c2.Session.Id); + Assert.StartsWith("uuid:", c1.Session.Id); + } + finally + { + c1.Abort(); + c2.Abort(); + } + } + + [WcfFact] + public static void Factory_CreateChannel_PreservesAddresses() + { + IChannelFactory factory = BuildFactory(); + factory.Open(); + var ea = new EndpointAddress("net.msmq://localhost/private/scratch"); + IOutputSessionChannel channel = factory.CreateChannel(ea); + try + { + Assert.Same(ea, channel.RemoteAddress); + Assert.Equal(new Uri("net.msmq://localhost/private/scratch"), channel.Via); + } + finally + { + channel.Abort(); + } + } + + [WcfFact] + public static void Factory_GetProperty_MessageVersion() + { + IChannelFactory factory = BuildFactory(); + Assert.Equal(MessageVersion.Soap12WSAddressing10, factory.GetProperty()); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqScaffoldTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqScaffoldTest.cs new file mode 100644 index 00000000000..a228fa33083 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqScaffoldTest.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Reflection; +using Infrastructure.Common; +using Xunit; + +public static class MsmqScaffoldTest +{ + [WcfFact] + public static void Assembly_Loads() + { + Assembly asm = Assembly.Load("System.ServiceModel.Msmq"); + Assert.NotNull(asm); + Assert.Equal("System.ServiceModel.Msmq", asm.GetName().Name); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqSecurityTypesTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqSecurityTypesTest.cs new file mode 100644 index 00000000000..aa4abb58fbd --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqSecurityTypesTest.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Net.Security; +using System.ServiceModel; +using System.ServiceModel.Security; +using Infrastructure.Common; +using Xunit; + +public static class MsmqSecurityTypesTest +{ + [WcfFact] + public static void MsmqTransportSecurity_Defaults_MatchNetFx() + { + var s = new MsmqTransportSecurity(); + Assert.Equal(MsmqAuthenticationMode.WindowsDomain, s.MsmqAuthenticationMode); + Assert.Equal(MsmqEncryptionAlgorithm.RC4Stream, s.MsmqEncryptionAlgorithm); + Assert.Equal(MsmqSecureHashAlgorithm.Sha256, s.MsmqSecureHashAlgorithm); + Assert.Equal(ProtectionLevel.Sign, s.MsmqProtectionLevel); + } + + [WcfFact] + public static void MsmqTransportSecurity_CopyCtor_CopiesAllProperties() + { + var original = new MsmqTransportSecurity + { + MsmqAuthenticationMode = MsmqAuthenticationMode.Certificate, + MsmqEncryptionAlgorithm = MsmqEncryptionAlgorithm.Aes, + MsmqSecureHashAlgorithm = MsmqSecureHashAlgorithm.Sha512, + MsmqProtectionLevel = ProtectionLevel.EncryptAndSign, + }; + var copy = new MsmqTransportSecurity(original); + Assert.Equal(original.MsmqAuthenticationMode, copy.MsmqAuthenticationMode); + Assert.Equal(original.MsmqEncryptionAlgorithm, copy.MsmqEncryptionAlgorithm); + Assert.Equal(original.MsmqSecureHashAlgorithm, copy.MsmqSecureHashAlgorithm); + Assert.Equal(original.MsmqProtectionLevel, copy.MsmqProtectionLevel); + } + + [WcfFact] + public static void MsmqTransportSecurity_CopyCtor_NullThrows() + { + Assert.Throws(() => new MsmqTransportSecurity(null)); + } + + [WcfTheory] + [InlineData((MsmqAuthenticationMode)42)] + [InlineData((MsmqAuthenticationMode)(-1))] + public static void MsmqTransportSecurity_InvalidAuthMode_Throws(MsmqAuthenticationMode bad) + { + var s = new MsmqTransportSecurity(); + Assert.Throws(() => s.MsmqAuthenticationMode = bad); + } + + [WcfTheory] + [InlineData((ProtectionLevel)42)] + public static void MsmqTransportSecurity_InvalidProtectionLevel_Throws(ProtectionLevel bad) + { + var s = new MsmqTransportSecurity(); + Assert.Throws(() => s.MsmqProtectionLevel = bad); + } + + [WcfFact] + public static void MessageSecurityOverMsmq_Defaults() + { + var m = new MessageSecurityOverMsmq(); + Assert.Equal(MessageCredentialType.Windows, m.ClientCredentialType); + Assert.Same(SecurityAlgorithmSuite.Default, m.AlgorithmSuite); + } + + [WcfFact] + public static void MessageSecurityOverMsmq_AlgorithmSuite_NullThrows() + { + var m = new MessageSecurityOverMsmq(); + Assert.Throws(() => m.AlgorithmSuite = null); + } + + [WcfTheory] + [InlineData((MessageCredentialType)42)] + public static void MessageSecurityOverMsmq_InvalidCredential_Throws(MessageCredentialType bad) + { + var m = new MessageSecurityOverMsmq(); + Assert.Throws(() => m.ClientCredentialType = bad); + } + + [WcfTheory] + [InlineData(DeadLetterQueue.None, true)] + [InlineData(DeadLetterQueue.System, true)] + [InlineData(DeadLetterQueue.Custom, true)] + [InlineData((DeadLetterQueue)42, false)] + public static void DeadLetterQueueHelper_IsDefined(DeadLetterQueue value, bool expected) + { + Type t = typeof(DeadLetterQueue).Assembly.GetType("System.ServiceModel.DeadLetterQueueHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic).Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } + + [WcfTheory] + [InlineData(QueueTransferProtocol.Native, true)] + [InlineData(QueueTransferProtocol.Srmp, true)] + [InlineData(QueueTransferProtocol.SrmpSecure, true)] + [InlineData((QueueTransferProtocol)42, false)] + public static void QueueTransferProtocolHelper_IsDefined(QueueTransferProtocol value, bool expected) + { + Type t = typeof(QueueTransferProtocol).Assembly.GetType("System.ServiceModel.QueueTransferProtocolHelper", throwOnError: true); + bool actual = (bool)t.GetMethod("IsDefined", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic).Invoke(null, new object[] { value }); + Assert.Equal(expected, actual); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqTransactionModeTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqTransactionModeTest.cs new file mode 100644 index 00000000000..15978218e87 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqTransactionModeTest.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Reflection; +using System.Runtime.Versioning; +using System.Transactions; +using Infrastructure.Common; +using Xunit; + +// Verifies the 3-way transaction-mode dispatch in MsmqMessagingInterop. +// Originally pinned to MSMQ.Messaging.MessageQueueTransactionType +// (slice 5c). Slice 9 replaced MSMQ.Messaging with a hand-rolled +// P/Invoke layer; the dispatch contract is unchanged but the enum +// is now System.ServiceModel.Channels.MsmqTransactionMode, which is +// internal. We reflect through it by name so the test doesn't have +// to make the enum public just to satisfy the assertion. +[SupportedOSPlatform("windows")] +public static class MsmqTransactionModeTest +{ + private static readonly Type s_modeType = + typeof(System.ServiceModel.NetMsmqBinding).Assembly + .GetType("System.ServiceModel.Channels.MsmqTransactionMode", throwOnError: true); + + private static readonly MethodInfo s_getMode = + typeof(System.ServiceModel.NetMsmqBinding).Assembly + .GetType("System.ServiceModel.Channels.MsmqMessagingInterop", throwOnError: true) + .GetMethod("GetTransactionMode", BindingFlags.Static | BindingFlags.NonPublic); + + private static int Invoke(bool exactlyOnce, Transaction ambient) + { + object result = s_getMode.Invoke(null, new object[] { exactlyOnce, ambient }); + return (int)result; + } + + private static int ModeValue(string name) => (int)Enum.Parse(s_modeType, name); + + [WcfFact] + public static void NonExactlyOnce_AnyAmbient_ReturnsNone() + { + int none = ModeValue("None"); + using var scope = new TransactionScope(TransactionScopeOption.RequiresNew); + Assert.Equal(none, Invoke(false, Transaction.Current)); + Assert.Equal(none, Invoke(false, null)); + } + + [WcfFact] + public static void ExactlyOnce_NoAmbient_ReturnsSingle() + { + Assert.Equal(ModeValue("Single"), Invoke(true, null)); + } + + [WcfFact] + public static void ExactlyOnce_WithAmbient_ReturnsAutomatic() + { + using var scope = new TransactionScope(TransactionScopeOption.RequiresNew); + Assert.Equal(ModeValue("Automatic"), Invoke(true, Transaction.Current)); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/MsmqUriTest.cs b/src/System.ServiceModel.Msmq/tests/MsmqUriTest.cs new file mode 100644 index 00000000000..33e747def50 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/MsmqUriTest.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Reflection; +using Infrastructure.Common; +using Xunit; + +public static class MsmqUriTest +{ + private static readonly Assembly s_msmqAsm = typeof(System.ServiceModel.NetMsmqBinding).Assembly; + private static readonly Type s_msmqUriType = s_msmqAsm.GetType("System.ServiceModel.Channels.MsmqUri", throwOnError: true); + + private static object GetTranslator(string name) + { + return s_msmqUriType.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); + } + + private static string Translate(object translator, Uri uri) + { + return (string)translator.GetType().GetMethod("UriToFormatName").Invoke(translator, new object[] { uri }); + } + + private static string TranslateByScheme(Uri uri) + { + MethodInfo m = s_msmqUriType.GetMethod("UriToFormatNameByScheme", BindingFlags.NonPublic | BindingFlags.Static); + return (string)m.Invoke(null, new object[] { uri }); + } + + [WcfTheory] + [InlineData("net.msmq://localhost/private/q1", "DIRECT=OS:.\\private$\\q1")] + [InlineData("net.msmq://localhost/q1", "DIRECT=OS:.\\q1")] + [InlineData("net.msmq://machine01/private/q1", "DIRECT=OS:machine01\\private$\\q1")] + [InlineData("net.msmq://machine01/q1", "DIRECT=OS:machine01\\q1")] + [InlineData("net.msmq://10.0.0.1/q1", "DIRECT=TCP:10.0.0.1\\q1")] + [InlineData("net.msmq://10.0.0.1/private/q1", "DIRECT=TCP:10.0.0.1\\private$\\q1")] + public static void NetMsmqAddressTranslator_FormatName(string uriString, string expected) + { + object translator = GetTranslator("NetMsmqAddressTranslator"); + Assert.Equal(expected, Translate(translator, new Uri(uriString))); + } + + [WcfFact] + public static void NetMsmqAddressTranslator_Scheme() + { + object translator = GetTranslator("NetMsmqAddressTranslator"); + Assert.Equal("net.msmq", translator.GetType().GetProperty("Scheme").GetValue(translator)); + } + + [WcfFact] + public static void NetMsmqAddressTranslator_RejectsWrongScheme() + { + object translator = GetTranslator("NetMsmqAddressTranslator"); + var ex = Assert.Throws(() => Translate(translator, new Uri("http://localhost/q1"))); + Assert.IsType(ex.InnerException); + } + + [WcfFact] + public static void NetMsmqAddressTranslator_RejectsLegacyPrivateDollar() + { + object translator = GetTranslator("NetMsmqAddressTranslator"); + var ex = Assert.Throws(() => Translate(translator, new Uri("net.msmq://localhost/private$/q1"))); + Assert.IsType(ex.InnerException); + } + + [WcfTheory] + [InlineData("net.msmq://server:8080/q1", "DIRECT=http://server:8080/msmq/q1")] + [InlineData("net.msmq://server/q1", "DIRECT=http://server/msmq/q1")] + [InlineData("net.msmq://server/private/q1", "DIRECT=http://server/msmq/private$/q1")] + public static void SrmpAddressTranslator_FormatName(string uriString, string expected) + { + object translator = GetTranslator("SrmpAddressTranslator"); + Assert.Equal(expected, Translate(translator, new Uri(uriString))); + } + + [WcfTheory] + [InlineData("net.msmq://server/q1", "DIRECT=https://server/msmq/q1")] + public static void SrmpsAddressTranslator_FormatName(string uriString, string expected) + { + object translator = GetTranslator("SrmpsAddressTranslator"); + Assert.Equal(expected, Translate(translator, new Uri(uriString))); + } + + [WcfTheory] + [InlineData("msmq.formatname:DIRECT=OS:.\\private$\\q1", "DIRECT=OS:.\\private$\\q1")] + [InlineData("msmq.formatname:DIRECT=TCP:10.0.0.1\\q1", "DIRECT=TCP:10.0.0.1\\q1")] + public static void FormatNameAddressTranslator_FormatName(string uriString, string expected) + { + object translator = GetTranslator("FormatNameAddressTranslator"); + Assert.Equal(expected, Translate(translator, new Uri(uriString))); + } + + [WcfFact] + public static void UriToFormatNameByScheme_DispatchesOnScheme() + { + Assert.Equal("DIRECT=OS:.\\q1", TranslateByScheme(new Uri("net.msmq://localhost/q1"))); + Assert.Equal("DIRECT=OS:.\\q1", TranslateByScheme(new Uri("msmq.formatname:DIRECT=OS:.\\q1"))); + } + + [WcfFact] + public static void UriToFormatNameByScheme_RejectsUnknownScheme() + { + var ex = Assert.Throws(() => TranslateByScheme(new Uri("ftp://localhost/q1"))); + Assert.IsType(ex.InnerException); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/NativeMsmqMessageTest.cs b/src/System.ServiceModel.Msmq/tests/NativeMsmqMessageTest.cs new file mode 100644 index 00000000000..3b674320995 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/NativeMsmqMessageTest.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Reflection; +using System.Runtime.Versioning; +using Infrastructure.Common; +using Xunit; + +// Marshaling-layout tests for the post-slice-9 native MSMQ message +// builder. The full send path needs a running MSMQ to verify, but +// the per-slot byte layout written into the MQPROPVARIANT buffer is +// pure logic and can be pinned cheaply with reflection + byte-level +// assertions. +// +// If any of these tests start failing it means the in-memory layout +// stopped matching what mqrt.dll expects, and the next live send +// will either return a property-validation error (best case) or +// silently corrupt the message body (worst case). +[SupportedOSPlatform("windows")] +public static class NativeMsmqMessageTest +{ + private static readonly Type s_native = + typeof(System.ServiceModel.NetMsmqBinding).Assembly + .GetType("System.ServiceModel.Channels.NativeMsmqMessage", throwOnError: true); + + private static object NewMessage() => Activator.CreateInstance(s_native, nonPublic: true); + + private static void InvokeSetter(object message, string name, params object[] args) + { + // Resolve overload by name + parameter count to avoid having + // to spell out the full parameter type list. + MethodInfo m = null; + foreach (var candidate in s_native.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (candidate.Name == name && candidate.GetParameters().Length == args.Length) + { + m = candidate; + break; + } + } + Assert.NotNull(m); + m.Invoke(message, args); + } + + private static byte[] GetSlot(object message, int index) + => (byte[])s_native.GetMethod("GetSlotForTests", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(message, new object[] { index }); + + private static uint GetPropId(object message, int index) + => (uint)s_native.GetMethod("GetPropIdForTests", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(message, new object[] { index }); + + private static int GetSlotCount(object message) + => (int)s_native.GetMethod("get_SlotCountForTests", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(message, null); + + private static int VectorOffset + => (int)s_native.GetMethod("get_VectorElementsOffsetForTests", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, null); + + private static int PropVarSize + => (int)s_native.GetMethod("get_PropVariantSizeForTests", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, null); + + // ----- Layout invariants ----- + + [WcfFact] + public static void PropVariantSize_Is_24Bytes() + { + // Treat the whole struct as a fixed 24-byte slot regardless of + // arch. The native union's largest member (DECIMAL / CAUB on x64) + // sets the upper bound, and the 24-byte assumption is what every + // slot writer in NativeMsmqMessage allocates against. + Assert.Equal(24, PropVarSize); + } + + [WcfFact] + public static void VectorElementsOffset_MatchesPointerWidth() + { + // Vector members store { cElems @ 8, pElems @ vectorOffset }. + // On x64 (8-byte pointer with 8-byte alignment) the pointer + // slot sits at offset 16; on x86 at offset 12. Anything else + // means the union layout was miscalculated. + Assert.Equal(IntPtr.Size == 8 ? 16 : 12, VectorOffset); + } + + // ----- Per-setter slot content ----- + + [WcfFact] + public static void SetByte_Writes_VT_UI1_AndValue() + { + object msg = NewMessage(); + InvokeSetter(msg, "SetByte", (uint)4u /* PROPID_M_PRIORITY */, (byte)5); + Assert.Equal(1, GetSlotCount(msg)); + Assert.Equal(4u, GetPropId(msg, 0)); + + byte[] slot = GetSlot(msg, 0); + Assert.Equal(24, slot.Length); + Assert.Equal(17, slot[0]); // VT_UI1 little-endian low byte + Assert.Equal(0, slot[1]); // VT_UI1 little-endian high byte + Assert.Equal(5, slot[8]); // value at union offset + } + + [WcfFact] + public static void SetUInt32_Writes_VT_UI4_AndLittleEndianValue() + { + object msg = NewMessage(); + InvokeSetter(msg, "SetUInt32", 14u /* PROPID_M_TIME_TO_BE_RECEIVED */, 0xCAFEBABEu); + byte[] slot = GetSlot(msg, 0); + Assert.Equal(19, slot[0]); // VT_UI4 + Assert.Equal(0, slot[1]); + Assert.Equal(0xBE, slot[8]); + Assert.Equal(0xBA, slot[9]); + Assert.Equal(0xFE, slot[10]); + Assert.Equal(0xCA, slot[11]); + } + + [WcfFact] + public static void SetByteVector_Writes_VT_VECTOR_BIT_AndLengthAtUnionStart() + { + object msg = NewMessage(); + InvokeSetter(msg, "SetByteVector", 24u /* PROPID_M_EXTENSION */, new byte[] { 1, 2, 3, 4 }, 0u); + byte[] slot = GetSlot(msg, 0); + // VT_VECTOR (0x1000) | VT_UI1 (17) = 0x1011 = 4113 + Assert.Equal(0x11, slot[0]); + Assert.Equal(0x10, slot[1]); + // cElems at offset 8 + Assert.Equal(4, slot[8]); + Assert.Equal(0, slot[9]); + Assert.Equal(0, slot[10]); + Assert.Equal(0, slot[11]); + // pElems pointer occupies VectorElementsOffset..VectorElementsOffset+IntPtr.Size. + // We can't assert its absolute value, but it must be non-zero. + bool anyNonZero = false; + for (int i = VectorOffset; i < VectorOffset + IntPtr.Size; i++) anyNonZero |= slot[i] != 0; + Assert.True(anyNonZero, "pElems pointer slot must be non-zero after SetByteVector"); + } + + [WcfFact] + public static void SetByteVector_LengthPropId_AppendsSecondSlot() + { + object msg = NewMessage(); + InvokeSetter(msg, "SetByteVector", 24u /* PROPID_M_EXTENSION */, new byte[] { 7, 8 }, 25u /* PROPID_M_EXTENSION_LEN */); + Assert.Equal(2, GetSlotCount(msg)); + Assert.Equal(24u, GetPropId(msg, 0)); + Assert.Equal(25u, GetPropId(msg, 1)); + // Length slot is VT_UI4 with value 2 + byte[] len = GetSlot(msg, 1); + Assert.Equal(19, len[0]); // VT_UI4 + Assert.Equal(2, len[8]); + } + + [WcfFact] + public static void SetWideString_WritesVT_LPWSTR_AndAppendsLength() + { + object msg = NewMessage(); + InvokeSetter(msg, "SetWideString", 11u /* PROPID_M_LABEL */, "demo", 12u /* PROPID_M_LABEL_LEN */); + Assert.Equal(2, GetSlotCount(msg)); + byte[] slot = GetSlot(msg, 0); + Assert.Equal(31, slot[0]); // VT_LPWSTR + // Length slot includes terminating null: 4 + 1 = 5 + byte[] len = GetSlot(msg, 1); + Assert.Equal(19, len[0]); + Assert.Equal(5, len[8]); + } + + [WcfFact] + public static void SetBody_AppendsBodySlot_PlusBodySize() + { + object msg = NewMessage(); + InvokeSetter(msg, "SetBody", new byte[] { 0xA0, 0xA1, 0xA2 }, 0, 3); + Assert.Equal(2, GetSlotCount(msg)); + Assert.Equal(9u, GetPropId(msg, 0)); // PROPID_M_BODY + Assert.Equal(10u, GetPropId(msg, 1)); // PROPID_M_BODY_SIZE + + byte[] body = GetSlot(msg, 0); + Assert.Equal(0x11, body[0]); // VT_VECTOR | VT_UI1 low byte + Assert.Equal(0x10, body[1]); // VT_VECTOR | VT_UI1 high byte + Assert.Equal(3, body[8]); // cElems + + byte[] size = GetSlot(msg, 1); + Assert.Equal(19, size[0]); // VT_UI4 + Assert.Equal(3, size[8]); + } + + // ----- Message-id parser (used for CorrelationId) ----- + + [WcfFact] + public static void ParseMessageId_Valid_Returns20Bytes() + { + MethodInfo m = s_native.GetMethod("ParseMessageId", BindingFlags.Static | BindingFlags.NonPublic); + byte[] result = (byte[])m.Invoke(null, new object[] { "11111111-2222-3333-4444-555555555555\\7" }); + Assert.Equal(20, result.Length); + // Counter is the trailing 4 bytes, little-endian. + Assert.Equal(7, result[16]); + Assert.Equal(0, result[17]); + Assert.Equal(0, result[18]); + Assert.Equal(0, result[19]); + } + + [WcfTheory] + [InlineData(null)] + [InlineData("")] + [InlineData("no-backslash")] + [InlineData("not-a-guid\\1")] + [InlineData("11111111-2222-3333-4444-555555555555\\not-a-number")] + [InlineData("11111111-2222-3333-4444-555555555555\\")] + public static void ParseMessageId_Invalid_Throws(string input) + { + MethodInfo m = s_native.GetMethod("ParseMessageId", BindingFlags.Static | BindingFlags.NonPublic); + TargetInvocationException ex = Assert.Throws(() => m.Invoke(null, new object[] { input })); + Assert.IsType(ex.InnerException); + } +} diff --git a/src/System.ServiceModel.Msmq/tests/System.ServiceModel.Msmq.Tests.csproj b/src/System.ServiceModel.Msmq/tests/System.ServiceModel.Msmq.Tests.csproj new file mode 100644 index 00000000000..55089a327a3 --- /dev/null +++ b/src/System.ServiceModel.Msmq/tests/System.ServiceModel.Msmq.Tests.csproj @@ -0,0 +1,15 @@ + + + $(UnitTestTargetFrameworks) + false + true + false + + + + + + + + + diff --git a/src/System.ServiceModel.NetNamedPipe/src/System/ServiceModel/AddressAccessDeniedException.cs b/src/System.ServiceModel.NetNamedPipe/src/System/ServiceModel/AddressAccessDeniedException.cs index ebd166af1cc..01ca3baeb55 100644 --- a/src/System.ServiceModel.NetNamedPipe/src/System/ServiceModel/AddressAccessDeniedException.cs +++ b/src/System.ServiceModel.NetNamedPipe/src/System/ServiceModel/AddressAccessDeniedException.cs @@ -1,18 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -using System.Runtime.Serialization; -namespace System.ServiceModel -{ - [Serializable] - public class AddressAccessDeniedException : CommunicationException - { - public AddressAccessDeniedException() { } - public AddressAccessDeniedException(string message) : base(message) { } - public AddressAccessDeniedException(string message, Exception innerException) : base(message, innerException) { } -#pragma warning disable SYSLIB0051 - protected AddressAccessDeniedException(SerializationInfo info, StreamingContext context) : base(info, context) { } -#pragma warning restore SYSLIB0051 - } -} +using System.Runtime.CompilerServices; +using System.ServiceModel; + +// AddressAccessDeniedException lived in this package through 8.x. As of +// 10.x the type's home is System.ServiceModel.Primitives so other +// transports (notably MSMQ) can throw it without taking a coupling on +// NetNamedPipe. The TypeForwardedTo keeps existing consumers that +// reference our NetNamedPipe surface seeing the same type identity. +[assembly: TypeForwardedTo(typeof(AddressAccessDeniedException))] + diff --git a/src/System.ServiceModel.Primitives/ref/NetFxTypeForwards.cs b/src/System.ServiceModel.Primitives/ref/NetFxTypeForwards.cs index c646b6b7ee1..8350227e203 100644 --- a/src/System.ServiceModel.Primitives/ref/NetFxTypeForwards.cs +++ b/src/System.ServiceModel.Primitives/ref/NetFxTypeForwards.cs @@ -35,6 +35,7 @@ //[assembly: TypeForwardedTo(typeof(System.ServiceModel.ClientBase<>.EndOperationDelegate))] //[assembly: TypeForwardedTo(typeof(System.ServiceModel.ClientBase<>.InvokeAsyncCompletedEventArgs))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.ClientCredentialsSecurityTokenManager))] +[assembly: TypeForwardedTo(typeof(System.ServiceModel.AddressAccessDeniedException))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.CommunicationException))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.CommunicationObjectAbortedException))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.CommunicationObjectFaultedException))] diff --git a/src/System.ServiceModel.Primitives/ref/System.ServiceModel.Primitives.netstandard.cs b/src/System.ServiceModel.Primitives/ref/System.ServiceModel.Primitives.netstandard.cs index 04d7bc87ee3..835459a0497 100644 --- a/src/System.ServiceModel.Primitives/ref/System.ServiceModel.Primitives.netstandard.cs +++ b/src/System.ServiceModel.Primitives/ref/System.ServiceModel.Primitives.netstandard.cs @@ -382,6 +382,13 @@ public CommunicationException(string message) { } public CommunicationException(string message, System.Exception innerException) { } protected CommunicationException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } + public partial class AddressAccessDeniedException : System.ServiceModel.CommunicationException + { + public AddressAccessDeniedException() { } + public AddressAccessDeniedException(string message) { } + public AddressAccessDeniedException(string message, System.Exception innerException) { } + protected AddressAccessDeniedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } public partial class CommunicationObjectAbortedException : System.ServiceModel.CommunicationException { public CommunicationObjectAbortedException() { } diff --git a/src/System.ServiceModel.Primitives/src/NetFxTypeForwards.cs b/src/System.ServiceModel.Primitives/src/NetFxTypeForwards.cs index c646b6b7ee1..8350227e203 100644 --- a/src/System.ServiceModel.Primitives/src/NetFxTypeForwards.cs +++ b/src/System.ServiceModel.Primitives/src/NetFxTypeForwards.cs @@ -35,6 +35,7 @@ //[assembly: TypeForwardedTo(typeof(System.ServiceModel.ClientBase<>.EndOperationDelegate))] //[assembly: TypeForwardedTo(typeof(System.ServiceModel.ClientBase<>.InvokeAsyncCompletedEventArgs))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.ClientCredentialsSecurityTokenManager))] +[assembly: TypeForwardedTo(typeof(System.ServiceModel.AddressAccessDeniedException))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.CommunicationException))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.CommunicationObjectAbortedException))] [assembly: TypeForwardedTo(typeof(System.ServiceModel.CommunicationObjectFaultedException))] diff --git a/src/System.ServiceModel.Primitives/src/System/ServiceModel/AddressAccessDeniedException.cs b/src/System.ServiceModel.Primitives/src/System/ServiceModel/AddressAccessDeniedException.cs new file mode 100644 index 00000000000..a90cde7029f --- /dev/null +++ b/src/System.ServiceModel.Primitives/src/System/ServiceModel/AddressAccessDeniedException.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Runtime.Serialization; + +namespace System.ServiceModel +{ + // Thrown when an operation fails because the caller lacks the + // required permissions on a transport endpoint — typically an + // ACL on a named pipe or an MSMQ queue. Lives in + // System.ServiceModel.Primitives so transports that need the + // type don't have to take a coupling on each other. + // + // Lived in System.ServiceModel.NetNamedPipe through 8.x; promoted + // here so the MSMQ transport (which surfaces the same semantic + // for MQ_ERROR_ACCESS_DENIED and MQ_ERROR_SHARING_VIOLATION) can + // map to it without re-defining the type or taking a back-reference + // on NetNamedPipe. NetNamedPipe retains a [TypeForwardedTo] so + // existing consumers see no API change. + [Serializable] + public class AddressAccessDeniedException : CommunicationException + { + public AddressAccessDeniedException() { } + public AddressAccessDeniedException(string message) : base(message) { } + public AddressAccessDeniedException(string message, Exception innerException) : base(message, innerException) { } +#pragma warning disable SYSLIB0051 + protected AddressAccessDeniedException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 + } +} diff --git a/src/System.ServiceModel.Primitives/src/netstandard.cs b/src/System.ServiceModel.Primitives/src/netstandard.cs index d02d064fade..41f78aca0e0 100644 --- a/src/System.ServiceModel.Primitives/src/netstandard.cs +++ b/src/System.ServiceModel.Primitives/src/netstandard.cs @@ -583,6 +583,13 @@ public CommunicationException(string message) { } public CommunicationException(string message, System.Exception innerException) { } protected CommunicationException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } + public partial class AddressAccessDeniedException : System.ServiceModel.CommunicationException + { + public AddressAccessDeniedException() { } + public AddressAccessDeniedException(string message) { } + public AddressAccessDeniedException(string message, System.Exception innerException) { } + protected AddressAccessDeniedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } public partial class CommunicationObjectAbortedException : System.ServiceModel.CommunicationException { public CommunicationObjectAbortedException() { }