我们使用 Praxedo 并需要将其与我们的其他解决方案集成。 他们的 API 需要使用 SOAP,而且还需要 MTOM 和基本身份验证。
我们已成功与多种服务集成,例如他们的客户经理。 对于客户经理,我可以像这样创建客户经理客户端,并且它可以工作:
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:
使用系统; 使用System.Net; 使用 System.ServiceModel; 使用 System.ServiceModel.Channels; 使用 System.Text;
命名空间 Common.Configurations { 公共类 PraxedoSettings { 公共字符串用户名{获取;在里面; } 公共字符串密码{获取;在里面; }
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.<CreateGenericTask>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 GenericAdapter`1.BlockUntilCompleted()
> NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter)
> AsyncToSyncAdapter.Await(Func`1 invoke)
> TestMethodCommand.Execute(TestExecutionContext context)
> <>c__DisplayClass4_0.<PerformWork>b__0()
> <>c__DisplayClass1_0`1.<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](Func`1 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.<CreateGenericTask>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 GenericAdapter`1.BlockUntilCompleted()
> NoMessagePumpStrategy.WaitForCompletion(AwaitAdapter awaiter)
> AsyncToSyncAdapter.Await(Func`1 invoke)
> TestMethodCommand.Execute(TestExecutionContext context)
> <>c__DisplayClass4_0.<PerformWork>b__0()
> <>c__DisplayClass1_0`1.<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](Func`1 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 '接受编码:gzip,deflate'
--header '内容类型:内容类型:多部分/相关;类型=“应用程序/xop+xml”;边界=“无论什么”'
--header '授权:基本XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=='
--header '主机:eu1.praxedo.com'
--数据原始'--无论如何
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
我们终于成功了!
我们创建了一个值对象来捕获有关文件的信息:
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成功发送的请求。