在 C# 中,如何为 Praxedo 业务事件附件管理器创建 SOAP 集成?

In C#, how can I create a SOAP integration for the Praxedo Business Event Attachment Manager?

我们使用 Praxedo 并需要将其与我们的其他解决方案集成。 他们API需要使用SOAP,而且需要MTOM和Basic认证。

我们已经成功地集成了多项服务,例如他们的客户经理。对于 Customer Manager,我可以像这样创建 Customer Manager 客户端,并且它有效:

                    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();

PraxedoSettings 看起来像:

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;
        }
    }
}

但是,对于 Business Event Attachment Manager 客户端,类似的解决方案会导致:

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()

我们能够确定我们可以通过将 ContentType 添加到 HttpRequestMessageProperty 来解决此策略问题,如下所示:

            _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()

通过在 Postman 中进行修改,我们发现我们可以通过添加内容类型和内容的边界来创建成功的请求,如下所示:

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>'

但这看起来很老套,我们还不清楚如何在 C# 上下文中将边界值添加到 XML 之前的请求正文,而无需手动重新创建我们创建的所有逻辑应该从导入 WSDL 中获取。

有没有一种方法可以让我们在不应该有边界值的 ContentType 中进行通信?或者是否有一种“正常”的方式将这个边界插入到请求中,即使它似乎是错误的(对我来说)在正文中有非Xml的东西?

(我也忍不住觉得我们进行身份验证的方式可能本质上是错误的。为什么我们需要实例化 OperationContextScope 即使我们不使用或以其他方式捕获它的值?为什么我们需要多次从设置中取出用户名和密码并以多种方式呈现?)

P.S。在 Postman 中的进一步实验表明,如果我们仅使用内容类型 type=\"application/xop+xml\",则不需要边界,但回到 C# 中,如果我们将此值用作内容类型,我们将返回:

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

我们终于成功了!

我们创建了一个值对象来捕获有关文件的信息:

    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
            };
    }

我们修改了请求信封的一个实例,使其看起来像这样:

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
                    );
    }

我们可以使用 ToRestRequest() 创建一个可以从 RestClient 成功发送的请求。