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

Posted

技术标签:

【中文标题】在 C# 中,如何为 Praxedo 业务事件附件管理器创建 SOAP 集成?【英文标题】:In C#, how can I create a SOAP integration for the Praxedo Business Event Attachment Manager? 【发布时间】:2021-11-14 20:10:36 【问题描述】:

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

我们已成功集成了多项服务,例如他们的客户经理。对于客户经理,我可以像这样创建客户经理客户端,它可以工作:

                    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 来源:UnitTest1.cs 第 75 行持续时间:1 秒

消息:System.ServiceModel.FaultException:这些策略 不能满足的替代方案: http://schemas.xmlsoap.org/ws/2004/09/policy/optimizedmimeserializationOptimizedMimeSerialization

堆栈跟踪:ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) ServiceChannel.EndCall(String action, Object[] 出局,IAsyncResult 结果) c__DisplayClass1_0.b__0(IAsyncResult asyncResult) --- 来自先前位置的堆栈跟踪结束 --- AttachmentControllerV6.GetAttachments(String businessEventId) 第 38 行 AttachmentControllerV6.HasAttachments(String businessEventId) 第 22 行 Tests.AttachmentList() 第 78 行 GenericAdapter1.BlockUntilCompleted() NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter) AsyncToSyncAdapter.Await(Func1 调用) TestMethodCommand.Execute(TestExecutionContext 上下文) c__DisplayClass4_0.b__0() c__DisplayClass1_01.&lt;DoIsolated&gt;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;

但这会导致:

AttachmentList 来源:UnitTest1.cs 第 75 行持续时间:486 毫秒

消息:System.ServiceModel.FaultException:无法确定 消息的边界!

堆栈跟踪:ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) ServiceChannel.EndCall(String action, Object[] 出局,IAsyncResult 结果) c__DisplayClass1_0.b__0(IAsyncResult asyncResult) --- 来自先前位置的堆栈跟踪结束 --- AttachmentControllerV6.GetAttachments(String businessEventId) 第 38 行 AttachmentControllerV6.HasAttachments(String businessEventId) 第 22 行 Tests.AttachmentList() 第 78 行 GenericAdapter1.BlockUntilCompleted() NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter) AsyncToSyncAdapter.Await(Func1 调用) TestMethodCommand.Execute(TestExecutionContext 上下文) c__DisplayClass4_0.b__0() c__DisplayClass1_01.&lt;DoIsolated&gt;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,即使我们不使用或以其他方式捕获它的值?为什么我们需要多次从设置中获取用户名和密码并以多种方式呈现?)

附:在 Postman 中的进一步实验表明,如果我们简单地使用内容类型 type=\"application/xop+xml\",我们不需要边界,但是在 C# 中,如果我们使用这个值作为内容类型,我们会回到:

消息:System.ServiceModel.FaultException:这些策略 不能满足的替代方案: http://schemas.xmlsoap.org/ws/2004/09/policy/optimizedmimeserializationOptimizedMimeSerialization

【问题讨论】:

【参考方案1】:

我们终于搞定了!

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

    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成功发送的请求。

【讨论】:

以上是关于在 C# 中,如何为 Praxedo 业务事件附件管理器创建 SOAP 集成?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 Word 文档加载项创建 AfterSave 事件

如何为输入附件视图的高度设置动画?

如何为项目构建高效的统一文件存储方案

如何在 C# 中使用 SpInprocRecoContext 识别语音事件?

如何为测试目的模拟网络故障(在 C# 中)?

在 C# 中,如何为具有多个嵌套数组的 JSON 对象建模?