Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace SimpleSOAPClient.Exceptions
{
/// <summary>
/// Thrown when a problem is encountered deserializing MTOM attachments.
/// </summary>
public class SoapAttachmentDeserializationException : SoapClientException
{
/// <inheritdoc />
public SoapAttachmentDeserializationException(string message) : base(message)
{
}

/// <inheritdoc />
public SoapAttachmentDeserializationException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
11 changes: 11 additions & 0 deletions src/SimpleSOAPClient/Models/SoapEnvelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,6 +46,13 @@ public class SoapEnvelope
/// </summary>
[XmlElement("Body")]
public SoapEnvelopeBody Body { get; set; }

/// <summary>
/// MTOM attachments of the SOAP message.
/// See https://www.w3.org/TR/soap12-mtom.
/// </summary>
[XmlIgnore]
public IDictionary<string, HttpContent> Attachments { get; set; }

/// <summary>
/// Initializes a new instance of <see cref="SoapEnvelope"/>
Expand Down
42 changes: 42 additions & 0 deletions src/SimpleSOAPClient/Models/Xop/Include.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Xml.Serialization;

namespace SimpleSOAPClient.Models.Xop
{

/// <summary>
/// A reference to an attachment to the SoapEnvelope; see https://www.w3.org/TR/soap12-mtom/
/// </summary>
[XmlType(Namespace = Namespace)]
public class Include
{
/// <summary>
/// The W3 namespace for XOP Include
/// </summary>
public const string Namespace = "http://www.w3.org/2004/08/xop/include";

/// <summary>
/// The Content Id of the associated attachment.
/// </summary>
[XmlIgnore]
public string ContentId { get; set; }

/// <summary>
/// Helper method to generate the href attribute when serialized to XML.
/// </summary>
[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);
}
}

}
}
6 changes: 6 additions & 0 deletions src/SimpleSOAPClient/SimpleSOAPClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net4.5'">
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
<Reference Include="System.Net.Http" />
</ItemGroup>

Expand All @@ -51,8 +52,13 @@
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'uap10.0' ">
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="5.4.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
</ItemGroup>

<Import Project="$(MSBuildSDKExtrasTargets)" Condition="Exists('$(MSBuildSDKExtrasTargets)')" />
</Project>
45 changes: 42 additions & 3 deletions src/SimpleSOAPClient/SoapClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,12 +36,14 @@ namespace SimpleSOAPClient
using Exceptions;
using Handlers;
using Models;


/// <summary>
/// The SOAP client that can be used to invoke SOAP Endpoints
/// </summary>
public class SoapClient : ISoapClient, IDisposable
{
private readonly Regex _cidRegex = new Regex("^<(.*)>$");
private readonly bool _disposeHttpClient = true;
private readonly List<ISoapHandler> _handlers = new List<ISoapHandler>();
private SoapClientSettings _settings;
Expand Down Expand Up @@ -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<string, HttpContent> attachments = new Dictionary<string, HttpContent>();
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);
Expand All @@ -218,6 +254,9 @@ await RunAfterHttpResponseHandlers(
{
responseEnvelope =
Settings.SerializationProvider.ToSoapEnvelope(responseXml);
#if !NETSTANDARD1_1
responseEnvelope.Attachments = attachments;
#endif
}
catch (SoapEnvelopeDeserializationException)
{
Expand Down
53 changes: 53 additions & 0 deletions tests/SimpleSOAPClient.Tests/MultipartResponseTests.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage> 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",
});
}
}
}
}