From 565629533bf03cd314be2145d89018c0c0e3a3b7 Mon Sep 17 00:00:00 2001 From: James Bench Date: Fri, 23 Nov 2018 11:52:23 +0000 Subject: [PATCH 1/4] Basic implementation with happy path test. --- src/SimpleSOAPClient/Models/SoapEnvelope.cs | 11 ++++ src/SimpleSOAPClient/SimpleSOAPClient.csproj | 12 +++++ src/SimpleSOAPClient/SoapClient.cs | 34 ++++++++++-- .../MultipartResponseTests.cs | 53 +++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs diff --git a/src/SimpleSOAPClient/Models/SoapEnvelope.cs b/src/SimpleSOAPClient/Models/SoapEnvelope.cs index 1bcfd32..86f6a93 100644 --- a/src/SimpleSOAPClient/Models/SoapEnvelope.cs +++ b/src/SimpleSOAPClient/Models/SoapEnvelope.cs @@ -21,6 +21,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #endregion + +using System.Collections.Generic; +using System.Net.Http; + namespace SimpleSOAPClient.Models { using System.Xml.Serialization; @@ -42,6 +46,13 @@ public class SoapEnvelope /// [XmlElement("Body")] public SoapEnvelopeBody Body { get; set; } + + /// + /// MTOM attachments of the SOAP message. + /// See https://www.w3.org/TR/soap12-mtom. + /// + [XmlIgnore] + public IDictionary Attachments { get; set; } /// /// Initializes a new instance of diff --git a/src/SimpleSOAPClient/SimpleSOAPClient.csproj b/src/SimpleSOAPClient/SimpleSOAPClient.csproj index 03e0f6b..cc5a1d6 100644 --- a/src/SimpleSOAPClient/SimpleSOAPClient.csproj +++ b/src/SimpleSOAPClient/SimpleSOAPClient.csproj @@ -43,6 +43,7 @@ + @@ -51,8 +52,19 @@ + + + + + + + + C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnet.webapi.client\5.2.6\lib\portable-wp8+netcore45+net45+wp81+wpa81\System.Net.Http.Formatting.dll + + + diff --git a/src/SimpleSOAPClient/SoapClient.cs b/src/SimpleSOAPClient/SoapClient.cs index 6573b18..adec1c5 100644 --- a/src/SimpleSOAPClient/SoapClient.cs +++ b/src/SimpleSOAPClient/SoapClient.cs @@ -21,6 +21,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #endregion + +using System.Text.RegularExpressions; + namespace SimpleSOAPClient { using System; @@ -33,12 +36,14 @@ namespace SimpleSOAPClient using Exceptions; using Handlers; using Models; + /// /// The SOAP client that can be used to invoke SOAP Endpoints /// public class SoapClient : ISoapClient, IDisposable { + private readonly Regex _cidRegex = new Regex("^<(.*)>$"); private readonly bool _disposeHttpClient = true; private readonly List _handlers = new List(); private SoapClientSettings _settings; @@ -198,12 +203,34 @@ await RunBeforeSoapEnvelopeSerializationHandlers( await RunBeforeHttpRequestHandlers( requestXml, url, action, trackingId, beforeSoapEnvelopeSerializationHandlersResult.State, handlersOrderedAsc, ct); - + var response = await HttpClient.SendAsync(beforeHttpRequestHandlersResult.Request, ct).ConfigureAwait(false); - + + IDictionary attachments = new Dictionary(); + if (response.Content.IsMimeMultipartContent()) + { + var multipart = await response.Content.ReadAsMultipartAsync(ct); + foreach (var content in multipart.Contents) + { + if (content.Headers.ContentType.MediaType == "application/xop+xml") + { + // This is the SoapEnvelope + response.Content = content; + } + else + { + var cidMatch = _cidRegex.Match(content.Headers.GetValues("Content-Id").First()); + if (cidMatch.Success) + { + attachments.Add(cidMatch.Groups[1].Value, content); + } + } + } + } + var handlersOrderedDesc = _handlers.OrderByDescending(e => e.Order).ToList(); - + var afterHttpResponseHandlersResult = await RunAfterHttpResponseHandlers( response, url, action, trackingId, beforeHttpRequestHandlersResult.State, handlersOrderedDesc, ct); @@ -218,6 +245,7 @@ await RunAfterHttpResponseHandlers( { responseEnvelope = Settings.SerializationProvider.ToSoapEnvelope(responseXml); + responseEnvelope.Attachments = attachments; } catch (SoapEnvelopeDeserializationException) { diff --git a/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs b/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs new file mode 100644 index 0000000..65d161f --- /dev/null +++ b/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs @@ -0,0 +1,53 @@ +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SimpleSOAPClient.Models; +using Xunit; + +namespace SimpleSOAPClient.Tests +{ + public class MultipartResponseTests + { + private const string AttachmentContents = "The Attachment Contents"; + private const string AttachmentContentId = "test-content@test.com"; + + [Fact] + public async Task Test() + { + var httpMessageHandler = new TestHttpMessageHandler(); + var httpClient = new HttpClient(httpMessageHandler); + var soapClient = new SoapClient(httpClient); + + var result = await soapClient.SendAsync("http://test.com", "", SoapEnvelope.Prepare()); + Assert.Equal(AttachmentContents, await result.Attachments[AttachmentContentId].ReadAsStringAsync()); + } + + + private class TestHttpMessageHandler : HttpMessageHandler + { + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var emptyEnvelope = new SoapEnvelopeSerializationProvider().ToXmlString(SoapEnvelope.Prepare()); + var multipartResponse = new MultipartContent("related") + { + new StringContent(emptyEnvelope, Encoding.UTF8, "application/xop+xml"), + new StringContent(AttachmentContents, Encoding.UTF8, "text/plain") + { + Headers = {{"Content-Id", $"<{AttachmentContentId}>"}} + } + }; + + return Task.FromResult(new HttpResponseMessage + { + Version = HttpVersion.Version11, + Content = multipartResponse, + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "OK", + }); + } + } + } +} \ No newline at end of file From 88cbae73652ef46c55bd0cdee8778fda4d953634 Mon Sep 17 00:00:00 2001 From: James Bench Date: Fri, 23 Nov 2018 12:46:51 +0000 Subject: [PATCH 2/4] Added a model for the XOP Include element. --- src/SimpleSOAPClient/Models/Xop/Include.cs | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/SimpleSOAPClient/Models/Xop/Include.cs diff --git a/src/SimpleSOAPClient/Models/Xop/Include.cs b/src/SimpleSOAPClient/Models/Xop/Include.cs new file mode 100644 index 0000000..9faa723 --- /dev/null +++ b/src/SimpleSOAPClient/Models/Xop/Include.cs @@ -0,0 +1,24 @@ +using System.Xml.Serialization; + +namespace SimpleSOAPClient.Models.Xop +{ + /// + /// A reference to an attachment to the SoapEnvelope; see https://www.w3.org/TR/soap12-mtom/ + /// + [XmlType(Namespace = "http://www.w3.org/2004/08/xop/include")] + public class Include + { + /// + /// The Content Id of the associated attachment. + /// + [XmlIgnore] + public string ContentId { get; set; } + + /// + /// Helper method to generate the href attribute when serialized to XML. + /// + [XmlAttribute(AttributeName = "href")] + public string ContentIdAsHref => "cid:" + ContentId; + + } +} \ No newline at end of file From 64f5ef747e87095bf7c53c789c319cda1981d5c1 Mon Sep 17 00:00:00 2001 From: James Bench Date: Mon, 26 Nov 2018 10:24:21 +0000 Subject: [PATCH 3/4] Small bit of tidying up. --- .../SoapAttachmentDeserializationException.cs | 17 ++++++++++++++ src/SimpleSOAPClient/Models/Xop/Include.cs | 22 +++++++++++++++++-- src/SimpleSOAPClient/SoapClient.cs | 8 ++++++- .../MultipartResponseTests.cs | 2 +- 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs diff --git a/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs b/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs new file mode 100644 index 0000000..5dd6e3b --- /dev/null +++ b/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs @@ -0,0 +1,17 @@ +using System; + +namespace SimpleSOAPClient.Exceptions +{ + public class SoapAttachmentDeserializationException : SoapClientException + { + /// + public SoapAttachmentDeserializationException(string message) : base(message) + { + } + + /// + public SoapAttachmentDeserializationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/SimpleSOAPClient/Models/Xop/Include.cs b/src/SimpleSOAPClient/Models/Xop/Include.cs index 9faa723..adb6de1 100644 --- a/src/SimpleSOAPClient/Models/Xop/Include.cs +++ b/src/SimpleSOAPClient/Models/Xop/Include.cs @@ -1,13 +1,20 @@ +using System; using System.Xml.Serialization; namespace SimpleSOAPClient.Models.Xop { + /// /// A reference to an attachment to the SoapEnvelope; see https://www.w3.org/TR/soap12-mtom/ /// - [XmlType(Namespace = "http://www.w3.org/2004/08/xop/include")] + [XmlType(Namespace = Namespace)] public class Include { + /// + /// The W3 namespace for XOP Include + /// + public const string Namespace = "http://www.w3.org/2004/08/xop/include"; + /// /// The Content Id of the associated attachment. /// @@ -18,7 +25,18 @@ public class Include /// Helper method to generate the href attribute when serialized to XML. /// [XmlAttribute(AttributeName = "href")] - public string ContentIdAsHref => "cid:" + ContentId; + public string ContentIdAsHref + { + get => "cid:" + ContentId; + set + { + if (!value.StartsWith("cid:", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("Content Id must start 'cid:'."); + } + ContentId = value.Substring(4, value.Length - 4); + } + } } } \ No newline at end of file diff --git a/src/SimpleSOAPClient/SoapClient.cs b/src/SimpleSOAPClient/SoapClient.cs index adec1c5..68a9f64 100644 --- a/src/SimpleSOAPClient/SoapClient.cs +++ b/src/SimpleSOAPClient/SoapClient.cs @@ -207,6 +207,7 @@ await RunBeforeHttpRequestHandlers( var response = await HttpClient.SendAsync(beforeHttpRequestHandlersResult.Request, ct).ConfigureAwait(false); + // Handle multipart responses IDictionary attachments = new Dictionary(); if (response.Content.IsMimeMultipartContent()) { @@ -215,16 +216,21 @@ await RunBeforeHttpRequestHandlers( { if (content.Headers.ContentType.MediaType == "application/xop+xml") { - // This is the SoapEnvelope + // This part contains the Soap Envelop XML response.Content = content; } else { + // Any other part is an attachment and should have a content id var cidMatch = _cidRegex.Match(content.Headers.GetValues("Content-Id").First()); if (cidMatch.Success) { attachments.Add(cidMatch.Groups[1].Value, content); } + else + { + throw new SoapAttachmentDeserializationException("Multipart message part without content id found; all attachments much have a content id."); + } } } } diff --git a/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs b/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs index 65d161f..3eef619 100644 --- a/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs +++ b/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs @@ -14,7 +14,7 @@ public class MultipartResponseTests private const string AttachmentContentId = "test-content@test.com"; [Fact] - public async Task Test() + public async Task TestReceiveAndDeserializeSingleAttachment() { var httpMessageHandler = new TestHttpMessageHandler(); var httpClient = new HttpClient(httpMessageHandler); From 8e9b83a31fb1ccb8111db7e3b37edb19c9dbe31f Mon Sep 17 00:00:00 2001 From: James Bench Date: Mon, 26 Nov 2018 11:26:04 +0000 Subject: [PATCH 4/4] Tidyup dotnet standard 1.1 support (or lack of). --- .../Exceptions/SoapAttachmentDeserializationException.cs | 3 +++ src/SimpleSOAPClient/SimpleSOAPClient.csproj | 6 ------ src/SimpleSOAPClient/SoapClient.cs | 5 +++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs b/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs index 5dd6e3b..47284ff 100644 --- a/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs +++ b/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs @@ -2,6 +2,9 @@ namespace SimpleSOAPClient.Exceptions { + /// + /// Thrown when a problem is encountered deserializing MTOM attachments. + /// public class SoapAttachmentDeserializationException : SoapClientException { /// diff --git a/src/SimpleSOAPClient/SimpleSOAPClient.csproj b/src/SimpleSOAPClient/SimpleSOAPClient.csproj index cc5a1d6..459e352 100644 --- a/src/SimpleSOAPClient/SimpleSOAPClient.csproj +++ b/src/SimpleSOAPClient/SimpleSOAPClient.csproj @@ -60,11 +60,5 @@ - - - C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnet.webapi.client\5.2.6\lib\portable-wp8+netcore45+net45+wp81+wpa81\System.Net.Http.Formatting.dll - - - diff --git a/src/SimpleSOAPClient/SoapClient.cs b/src/SimpleSOAPClient/SoapClient.cs index 68a9f64..008f658 100644 --- a/src/SimpleSOAPClient/SoapClient.cs +++ b/src/SimpleSOAPClient/SoapClient.cs @@ -208,6 +208,8 @@ await RunBeforeHttpRequestHandlers( await HttpClient.SendAsync(beforeHttpRequestHandlersResult.Request, ct).ConfigureAwait(false); // Handle multipart responses + // DotNet Standard 1.1 isn't supported by the multipart library +#if !NETSTANDARD1_1 IDictionary attachments = new Dictionary(); if (response.Content.IsMimeMultipartContent()) { @@ -234,6 +236,7 @@ await RunBeforeHttpRequestHandlers( } } } +#endif var handlersOrderedDesc = _handlers.OrderByDescending(e => e.Order).ToList(); @@ -251,7 +254,9 @@ await RunAfterHttpResponseHandlers( { responseEnvelope = Settings.SerializationProvider.ToSoapEnvelope(responseXml); +#if !NETSTANDARD1_1 responseEnvelope.Attachments = attachments; +#endif } catch (SoapEnvelopeDeserializationException) {