Home > Back-end >  In C#, how can I create a SOAP integration for the Praxedo Business Event Attachment Manager?
In C#, how can I create a SOAP integration for the Praxedo Business Event Attachment Manager?

Time:10-16

We use Praxedo and need to integrate it with our other solutions. Their API requires the use of SOAP, and moreover requires MTOM and Basic authentication.

We've successfully integrated with multiple services, such as their Customer Manager. In the case of the Customer Manager, I can create the Customer Manager client like this, and it works:

                    EndpointAddress endpoint = new(_praxedoSettings.CustomerManagerEndpoint);
                    MtomMessageEncoderBindingElement encoding = new(new TextMessageEncodingBindingElement
                    {
                        MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None)
                    });

                    CustomBinding customBinding = new(encoding, new HttpsTransportBindingElement());
                    _CustomerManagerClient = new CustomerManagerClient(customBinding, endpoint);
                    _praxedoSettings.AddAuthorizationTo(_CustomerManagerClient);

                    _ = new OperationContextScope(_CustomerManagerClient.InnerChannel);
                    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name]
                        = _praxedoSettings.ToHttpRequestMessageProperty();

where PraxedoSettings looks like:

using System;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;

namespace Common.Configurations
{
    public class PraxedoSettings
    {
        public string Username { get; init; }
        public string Password { get; init; }

        public Uri BusinessEventAttachmentManagerEndpoint { get; init; }
        public Uri BusinessEventManagerEndpoint { get; init; }
        public Uri CustomerManagerEndpoint { get; init; }
        public Uri FieldResourceManagerEndpoint { get; init; }
        public Uri LocationManagerEndpoint { get; init; }

        public ClientBase<TChannel> AddAuthorizationTo<TChannel>(ClientBase<TChannel> client)
            where TChannel : class
        {
            client.ClientCredentials.UserName.UserName = Username;
            client.ClientCredentials.UserName.Password = Password;
            return client;
        }

        public string ToBasicAuthorizationHeader() =>
            $" Basic {ToBase64()}";

        private string ToBase64() =>
            Convert.ToBase64String(ToAsciiEncoding());

        private byte[] ToAsciiEncoding() =>
            Encoding.ASCII.GetBytes($"{Username}:{Password}");

        public T ToCredentials<T>()
        {
            T credentials = (T)Activator.CreateInstance(typeof(T));
            Set(credentials, "login", Username);
            Set(credentials, "password", Password);
            return credentials;
        }

        private static T Set<T>(T credentials, string propertyName, string propertyValue)
        {
            typeof(T)
                .GetProperty(propertyName)
                .SetValue(credentials, propertyValue);

            return credentials;
        }

        public string ToCredentialString() =>
            $"{Username}|{Password}";

        public HttpRequestMessageProperty ToHttpRequestMessageProperty()
        {
            HttpRequestMessageProperty httpRequestMessageProperty = new();
            httpRequestMessageProperty.Headers[HttpRequestHeader.Authorization] = ToBasicAuthorizationHeader();
            return httpRequestMessageProperty;
        }
    }
}

However, in the case of the Business Event Attachment Manager client, a similar solution results in:

AttachmentList Source: UnitTest1.cs line 75 Duration: 1 sec

Message: System.ServiceModel.FaultException : These policy alternatives can not be satisfied: {http://schemas.xmlsoap.org/ws/2004/09/policy/optimizedmimeserialization}OptimizedMimeSerialization

Stack Trace: ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) ServiceChannel.EndCall(String action, Object[] outs, IAsyncResult result) <>c__DisplayClass1_0.b__0(IAsyncResult asyncResult) --- End of stack trace from previous location --- AttachmentControllerV6.GetAttachments(String businessEventId) line 38 AttachmentControllerV6.HasAttachments(String businessEventId) line 22 Tests.AttachmentList() line 78 GenericAdapter1.BlockUntilCompleted() NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter) AsyncToSyncAdapter.Await(Func1 invoke) TestMethodCommand.Execute(TestExecutionContext context) <>c__DisplayClass4_0.b__0() <>c__DisplayClass1_01.<DoIsolated>b__0(Object _) ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) ContextUtils.DoIsolated(ContextCallback callback, Object state) ContextUtils.DoIsolated[T](Func1 func) SimpleWorkItem.PerformWork()

We were able to determine that we can solve this policy problem by adding the ContentType to the HttpRequestMessageProperty, like this:

            _praxedoSettings.AddAuthorizationTo(ManagerClient);

            _ = new OperationContextScope(ManagerClient.InnerChannel);
            HttpRequestMessageProperty httpRequestMessageProperty = _praxedoSettings.ToHttpRequestMessageProperty();
            httpRequestMessageProperty.Headers[HttpRequestHeader.ContentType] = "multipart/related; type=\"application/xop xml\"";
            OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name]
                = httpRequestMessageProperty;

But this results in:

AttachmentList Source: UnitTest1.cs line 75 Duration: 486 ms

Message: System.ServiceModel.FaultException : Couldn't determine the boundary from the message!

Stack Trace: ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) ServiceChannel.EndCall(String action, Object[] outs, IAsyncResult result) <>c__DisplayClass1_0.b__0(IAsyncResult asyncResult) --- End of stack trace from previous location --- AttachmentControllerV6.GetAttachments(String businessEventId) line 38 AttachmentControllerV6.HasAttachments(String businessEventId) line 22 Tests.AttachmentList() line 78 GenericAdapter1.BlockUntilCompleted() NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter) AsyncToSyncAdapter.Await(Func1 invoke) TestMethodCommand.Execute(TestExecutionContext context) <>c__DisplayClass4_0.b__0() <>c__DisplayClass1_01.<DoIsolated>b__0(Object _) ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) ContextUtils.DoIsolated(ContextCallback callback, Object state) ContextUtils.DoIsolated[T](Func1 func) SimpleWorkItem.PerformWork()

By hacking around in Postman, we've discovered we can create a successful request by adding a boundary both the content type and the content, like so:

curl --location --request POST 'https://eu1.praxedo.com/eTech/services/cxf/v6/BusinessEventAttachmentManager' \
--header 'Accept-Encoding:  gzip,deflate' \
--header 'Content-Type: Content-Type: multipart/related; type="application/xop xml"; boundary="whatever"' \
--header 'Authorization:  Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==' \
--header 'Host:  eu1.praxedo.com' \
--data-raw '--whatever


<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:bus="http://ws.praxedo.com/v6/businessEvent">
  <soap:Header/>
  <soap:Body>
    <bus:listAttachments>
      <businessEventId>00044</businessEventId>
    </bus:listAttachments>
  </soap:Body>
</soap:Envelope>'

But this seems very hacky and it is far from clear how we could add the boundary value to the body of the request before the XML from within the C# context, without manually recreating all the logic which we should be getting from importing the WSDL.

Is there a way we can communicate in the ContentType there shouldn't be a boundary value? Or is there a "normal" way to insert this boundary into the request, even though it seems wrong (to me) have something non-Xml in the body?

(I also can't help feeling that the we way we are doing authentication may be inherently wrong. Why do we need to instantiate OperationContextScope even though we don't use or otherwise capture its value? Why do we need to get the username and password out of the settings multiple times and present it multiple ways?)

P.S. Further experimenting in Postman has demonstrated we don't need the boundary if we simply use the content type type=\"application/xop xml\", BUT back in C#, if we use this value for the content type, we are back to:

Message: System.ServiceModel.FaultException : These policy alternatives can not be satisfied: {http://schemas.xmlsoap.org/ws/2004/09/policy/optimizedmimeserialization}OptimizedMimeSerialization

CodePudding user response:

We finally got this working!

We created a value object to capture information about the file:

    public class BusinessEventAttachmentFile
    {
        public string BusinessEventId { get; init; }
        public string FileName { get; init; }
        public string ContentType { get; init; } = "application/pdf";
        public byte[] FileBytes { get; init; }

        public BusinessEventAttachmentFile ToDeleteFile() =>
            new()
            {
                BusinessEventId = BusinessEventId,
                FileName = FileName
            };
    }

We modified an instance of the request envelope so it looks like this:

public partial class Envelope : IRequestEnvelope
    {
        private const string ContentType = "multipart/related; type=\"application/xop xml\"";

        public object Header { get; init; }

        public EnvelopeBody Body { get; init; }

        [XmlIgnore]
        private string StreamId { get; init; }

        [XmlIgnore]
        private BusinessEventAttachmentFile AttachmentFile;

        internal static Envelope From(BusinessEventAttachmentFile attachmentFile)
        {
            string streamId = Guid.NewGuid()
                .ToString();

            return new()
            {
                AttachmentFile = attachmentFile,
                Body = new()
                {
                    createAttachment = new()
                    {
                        attachment = new()
                        {
                            entityId = attachmentFile.BusinessEventId,
                            name = attachmentFile.FileName
                        },
                        stream = attachmentFile.FileBytes
                    }
                },
                StreamId = streamId
            };
        }

        public IRestRequest ToRestRequest(PraxedoSettings praxedoSettings) =>
            new RestRequest(Method.POST)
                .AddHeader("Content-Type", ContentType)
                .AddHeader("Authorization", praxedoSettings.ToBasicAuthorizationHeader())
                .AddParameter(ContentType, PraxedoSerializationHelper.CreateRequestBody(this), ParameterType.RequestBody)
                .AddFile(
                        name: StreamId,
                        bytes: AttachmentFile.FileBytes,
                        fileName: AttachmentFile.FileName,
                        contentType: AttachmentFile.ContentType
                    );
    }

We can use ToRestRequest() to create a request which can successfully be sent from RestClient.

  • Related