diff --git a/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs b/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs
new file mode 100644
index 0000000..47284ff
--- /dev/null
+++ b/src/SimpleSOAPClient/Exceptions/SoapAttachmentDeserializationException.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace SimpleSOAPClient.Exceptions
+{
+ ///
+ /// Thrown when a problem is encountered deserializing MTOM attachments.
+ ///
+ 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/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/Models/Xop/Include.cs b/src/SimpleSOAPClient/Models/Xop/Include.cs
new file mode 100644
index 0000000..adb6de1
--- /dev/null
+++ b/src/SimpleSOAPClient/Models/Xop/Include.cs
@@ -0,0 +1,42 @@
+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 = 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.
+ ///
+ [XmlIgnore]
+ public string ContentId { get; set; }
+
+ ///
+ /// Helper method to generate the href attribute when serialized to XML.
+ ///
+ [XmlAttribute(AttributeName = "href")]
+ 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/SimpleSOAPClient.csproj b/src/SimpleSOAPClient/SimpleSOAPClient.csproj
index 03e0f6b..459e352 100644
--- a/src/SimpleSOAPClient/SimpleSOAPClient.csproj
+++ b/src/SimpleSOAPClient/SimpleSOAPClient.csproj
@@ -43,6 +43,7 @@
+
@@ -51,8 +52,13 @@
+
+
+
+
+
diff --git a/src/SimpleSOAPClient/SoapClient.cs b/src/SimpleSOAPClient/SoapClient.cs
index 6573b18..008f658 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,43 @@ await RunBeforeSoapEnvelopeSerializationHandlers(
await RunBeforeHttpRequestHandlers(
requestXml, url, action, trackingId,
beforeSoapEnvelopeSerializationHandlersResult.State, handlersOrderedAsc, ct);
-
+
var response =
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())
+ {
+ var multipart = await response.Content.ReadAsMultipartAsync(ct);
+ foreach (var content in multipart.Contents)
+ {
+ if (content.Headers.ContentType.MediaType == "application/xop+xml")
+ {
+ // 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.");
+ }
+ }
+ }
+ }
+#endif
+
var handlersOrderedDesc = _handlers.OrderByDescending(e => e.Order).ToList();
-
+
var afterHttpResponseHandlersResult =
await RunAfterHttpResponseHandlers(
response, url, action, trackingId, beforeHttpRequestHandlersResult.State, handlersOrderedDesc, ct);
@@ -218,6 +254,9 @@ await RunAfterHttpResponseHandlers(
{
responseEnvelope =
Settings.SerializationProvider.ToSoapEnvelope(responseXml);
+#if !NETSTANDARD1_1
+ responseEnvelope.Attachments = attachments;
+#endif
}
catch (SoapEnvelopeDeserializationException)
{
diff --git a/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs b/tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs
new file mode 100644
index 0000000..3eef619
--- /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 TestReceiveAndDeserializeSingleAttachment()
+ {
+ 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