I need to put custom headers into WCF. My Code is as follows:
ServiceReference1.Service2Client ws = new Service2Client();
using (OperationContextScope scope = new OperationContextScope((IContextChannel)ws.InnerChannel))
{
MessageHeaders messageHeadersElement = OperationContext.Current.OutgoingMessageHeaders;
messageHeadersElement.Add(MessageHeader.CreateHeader("Authorization", String.Empty, "string"));
messageHeadersElement.Add(MessageHeader.CreateHeader("username", String.Empty, "user"));
var res = ws.GetUser("123");
}
But when I try to read it in the service, nothing is availabe in the following
public class OAuthAuthorizationManager : ServiceAuthorizationManager
{
protected override bool CheckAccessCore(OperationContext operationContext)
{
int index = OperationContext.Current.IncomingMessageHeaders.FindHeader("username", String.Empty);
string auth = operationContext.IncomingMessageHeaders.GetHeader<string>("username", String.Empty);
var hereIseeIt = operationContext.RequestContext.RequestMessage;
index is -1: not found
auth: is also displaying an exception that the header is not available
hereIseeIt: .ToString() shows a xml where I can see that user is existent, but I see no way to access that information in any of the objects
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<username xmlns="http://Microsoft.WCF.Documentation">user</username>
</s:Header>
<s:Body>
<GetUser xmlns="http://tempuri.org/">
<UserId>123</UserId>
</GetUser>
</s:Body>
</s:Envelope>
But I cannot access them since I find no way to access the s:Header ...
try using:
XPathNavigator XPN = operationContext.RequestContext.RequestMessage.CreateBufferedCopy ().CreateNavigator ();
NOT elegant but it gives you the whole Message accessible through a XPathNavigator which should make it easy to get to any value inside the Message you want..
some links:
http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.requestcontext.aspx
http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.message.aspx
http://msdn.microsoft.com/en-us/library/system.xml.xpath.xpathnavigator.aspx
Here's an easy way to get the inner XML of the username header for your scenario. Even if you already solved your issue a long time ago, I thought it might help somebody else who faces the same issue.
var username = String.Empty;
// using the namespace from you XML sample
var usernameHeaderPosition = OperationContext.Current
.IncomingMessageHeaders
.FindHeader("username", "http://Microsoft.WCF.Documentation");
if (usernameHeaderPosition > -1)
{
username = OperationContext.Current
.IncomingMessageHeaders
.GetReaderAtHeader(usernameHeaderPosition).ReadInnerXml();
}
Related
I am creating a webhook in a .Net Core 3 Web API for DocuSign Connect to invoke and provide me status updates + signed documents from envelopes my app has created. The C# example at https://www.docusign.com/blog/dsdev-adding-webhooks-application was very helpful in getting me almost to my goal. The code from the example is:
[HttpPost("api/[controller]/ConnectWebHook")]
public void ConnectWebHook(HttpRequestMessage request)
{
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(request.Content.ReadAsStreamAsync().Result);
var mgr = new XmlNamespaceManager(xmldoc.NameTable);
mgr.AddNamespace("a", "http://www.docusign.net/API/3.0");
XmlNode envelopeStatus = xmldoc.SelectSingleNode("//a:EnvelopeStatus", mgr);
XmlNode envelopeId = envelopeStatus.SelectSingleNode("//a:EnvelopeID", mgr);
XmlNode status = envelopeStatus.SelectSingleNode("./a:Status", mgr);
var targetFileDirectory = #"\\my-network-share\";
if (envelopeId != null)
{
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{status.InnerText}_.xml", xmldoc.OuterXml);
}
if (status.InnerText == "Completed")
{
// Loop through the DocumentPDFs element, storing each document.
XmlNode docs = xmldoc.SelectSingleNode("//a:DocumentPDFs", mgr);
foreach (XmlNode doc in docs.ChildNodes)
{
string documentName = doc.ChildNodes[0].InnerText; // pdf.SelectSingleNode("//a:Name", mgr).InnerText;
string documentId = doc.ChildNodes[2].InnerText; // pdf.SelectSingleNode("//a:DocumentID", mgr).InnerText;
string byteStr = doc.ChildNodes[1].InnerText; // pdf.SelectSingleNode("//a:PDFBytes", mgr).InnerText;
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{documentId}_{documentName}", byteStr);
}
}
}
For testing purposes, my Web API is allowing all origins and exposed to the outside world via NGROK, and I can hit other test endpoints (both GET and POST), but for some reason this webhook is not being hit by Connect when there is a notification-worthy event on my envelope.
I can see in the DocuSign Admin portal logs that Connect invoked my webhook but got The remote server returned an error: (415) Unsupported Media Type.. This led me to add the [FromBody] attribute to my method signature like so but I still get the same error when my webhook is invoked by Connect.
[HttpPost("api/[controller]/ConnectWebHook")]
public void ConnectWebHook([FromBody] HttpRequestMessage request)
{
// ... rest of the method was unchanged, removed for brevity
}
I have never used HttpRequestMessage before but it looks straightforward enough. I noticed in the DocuSign Admin portal logs that the data that Connect tried to send to the webhook is just XML. I could try to change the webhook's signature to look for an XmlDocument instead of an HttpRequestMessage but I am not sure what, if anything, I will be missing out on.
Has anyone else integrated with Connect via a webhook recently? And were you able to make the HttpRequestMessage work for you?
Added on 10/18/2019:
DocuSign mentions that the content type is XML. Here is what the content looks like:
<DocuSignEnvelopeInformation
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.docusign.net/API/3.0">
<EnvelopeStatus>...</EnvelopeStatus>
<DocumentPDFs>...</DocumentPDFs>
</DocuSignEnvelopeInformation>
I have added AddXmlSerializerFormatters() to the ConfigureServices method in Startup.cs. This being .Net Core 3, I had to set it up like services.AddControllers().AddXmlSerializerFormatters() instead of services.AddMVC().AddXmlSerializerFormatters() per https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.0&tabs=visual-studio.
With that change, I have now tried using [FromForm] like so and my webhook IS being hit, but the request input parameter is essentially empty ... request.Content = null:
[HttpPost("api/[controller]/ConnectWebHook")]
public void ConnectWebHook([FromForm] HttpRequestMessage request)
{
// ... rest of the method was unchanged, removed for brevity
}
Since the request is being sent from DocuSign Connect, I have no control over the headers/format/content. As far as I can tell, they are not submitting an XML object, not a form, so [FromForm] is probably not the way to go.
That linked example is not for .net core. HttpRequestMessage is no longer a first class citizen in asp.net-core framework and will treated as a normal model.
Just extract the content directly from the Request's body and the rest should be able to remain the same as in the example.
[HttpPost("api/[controller]/ConnectWebHook")]
public IActionResult ConnectWebHook() {
Stream stream = Request.Body;
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(stream);
var mgr = new XmlNamespaceManager(xmldoc.NameTable);
mgr.AddNamespace("a", "http://www.docusign.net/API/3.0");
XmlNode envelopeStatus = xmldoc.SelectSingleNode("//a:EnvelopeStatus", mgr);
XmlNode envelopeId = envelopeStatus.SelectSingleNode("//a:EnvelopeID", mgr);
XmlNode status = envelopeStatus.SelectSingleNode("./a:Status", mgr);
var targetFileDirectory = #"\\my-network-share\";
if (envelopeId != null) {
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{status.InnerText}_.xml", xmldoc.OuterXml);
}
if (status.InnerText == "Completed") {
// Loop through the DocumentPDFs element, storing each document.
XmlNode docs = xmldoc.SelectSingleNode("//a:DocumentPDFs", mgr);
foreach (XmlNode doc in docs.ChildNodes) {
string documentName = doc.ChildNodes[0].InnerText; // pdf.SelectSingleNode("//a:Name", mgr).InnerText;
string documentId = doc.ChildNodes[2].InnerText; // pdf.SelectSingleNode("//a:DocumentID", mgr).InnerText;
string byteStr = doc.ChildNodes[1].InnerText; // pdf.SelectSingleNode("//a:PDFBytes", mgr).InnerText;
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{documentId}_{documentName}", byteStr);
}
}
return Ok();
}
I have a TwiML app with this code in the Connect action of the CallController. This code is taken straight from the Twilio demos.
[HttpPost]
public virtual ActionResult Connect(string phoneNumber, string called)
{
var response = new VoiceResponse();
var dial = new Dial(callerId: "+6138595????");
if (phoneNumber != null)
{
dial.Number(phoneNumber);
}
else
{
dial.Client("support_agent");
}
response.Dial(dial);
return TwiML(response);
}
When this is called it raises the error "Data at the root level is invalid. Line 1, position 1."
The XML this generates is
<?xml version="1.0" encoding="utf-8"?>
<Response>
<Dial callerId="+6138595????">
<Client>support_agent</Client>
</Dial>
</Response>
Twilio Evangelist here.
A quick question - is this happening every time the method is invoked, or only when specific inputs are provided? Needing to build the string manually is of course not desired. So I would like to get to the bottom of what triggered this result.
I have found I can fix it by replacing
return TwiML(response);
with
return new TwiMLResult(response.ToString(), new UTF8Encoding());
Appears to be some kind of encoding issue using the first method.
I had the same problem we solved it in our WebApi by skipping the Twilio sdk and generating the xml by ourselves.
I hope this will work for you too:
[HttpPost]
public virtual HttpResponseMessage Connect(string phoneNumber, string called)
{
string twiml = $"<?xml version=\"1.0\" encoding=\"utf-8\"?><Response><Dial callerId=\"{phoneNumber}\"><Client>support_agent</Client></Dial></Response>";
var xmlResponse = new HttpResponseMessage();
xmlResponse.Content = new StringContent(twiml, Encoding.UTF8, "text/xml");
return xmlResponse;
}
Please notice that there are no end of lines - "\n", \r", etc.
I am successfully working with a third party soap service. I have added a service reference to a soap web service which has auto generated the classes.
When an error occurs it returns a soap response like this:
<SOAP-ENV:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Client</faultcode>
<faultstring xsi:type="xsd:string">Error while reading parameters of method 'Demo'</faultstring>
<detail xsi:type="xsd:string">Invalid login or password. Connection denied.</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I can catch the error but not extract the detail. I have tried the following code:
catch (FaultException ex)
{
MessageFault msgFault = ex.CreateMessageFault();
var elm = msgFault.GetDetail<string>();
//throw Detail
}
However it Errors with the following as detail node is not an object:
Expecting element 'string' from namespace 'http://schemas.datacontract.org/2004/07/MyDemoNamespace'.. Encountered 'Text' with name '', namespace ''.
This is third party API so I cannot change the response.
The detail node of the message fault is expected to contain XML. The GetDetail will deserialize this XML into the given object.
As the contents is not XML it was possible to use this method.
You can however get access to the XML and read the innerXml value:
MessageFault msgFault = ex.CreateMessageFault();
var msg = msgFault.GetReaderAtDetailContents().Value;
This approached worked.
Here's a few methods I've found of extracting that detailed exception information from FaultExceptions
Get the String Contents of a Single Element
catch (FaultException e)
{
var errorElement = XElement.Parse(e.CreateMessageFault().GetReaderAtDetailContents().ReadOuterXml());
var errorDictionary = errorElement.Elements().ToDictionary(key => key.Name.LocalName, val => val.Value);
var errorMessage = errorDictionary?["ErrorMessage"];
}
Example Output:
Organization does not exist.
Get the String Contents of a All Details as a Single String
catch (FaultException e)
{
var errorElement = XElement.Parse(e.CreateMessageFault().GetReaderAtDetailContents().ReadOuterXml());
var errorDictionary = errorElement.Elements().ToDictionary(key => key.Name.LocalName, val => val.Value);
var errorDetails = string.Join(";", errorDictionary);
}
Example Output:
[ErrorMessage, Organization does not exist.];[EventCode, 3459046134826139648];[Parameters, ]
Get the String Contents of a Everything as an XML string
var errorElement = XElement.Parse(e.CreateMessageFault().GetReaderAtDetailContents().ReadOuterXml());
var xmlDetail = (string)errorElement;
Example Output:
<FaultData xmlns="http://schemas.datacontract.org/2004/07/Xata.Ignition.Common.Contract" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ErrorMessage>Organization does not exist.</ErrorMessage>
<EventCode>3459046134826139648</EventCode>
<Parameters i:nil="true" xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays"></Parameters>
</FaultData>
The following should give you the value of the detail element of the FaultException.
var faultMessage = faultException.CreateMessageFault();
if(faultMessage.HasDetail){
Console.Write(faultMessage.GetDetail<XElement>().Value);
}
public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
if (reply.IsFault)
{
// Create a copy of the original reply to allow default WCF processing
MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
Message copy = buffer.CreateMessage(); // Create a copy to work with
reply = buffer.CreateMessage(); // Restore the original message
MessageFault faultex = MessageFault.CreateFault(copy, Int32.MaxValue); //Get Fault from Message
FaultCode codigo = faultex.Code;
//if (faultex.HasDetail)... //More details
buffer.Close();
You can catch FaultException<TDetail>, which gives you detail for free.
catch (FaultException<string> ex)
{
string yourDetail = ex.Detail;
}
I have some code on my C# webservice, that checks the header string contains a valid user.
The code is:
MessageHeaders headers = OperationContext.Current.IncomingMessageHeaders;
String soapHeader = headers.GetHeader<String>("VerifyUser", "http://companyname.co.uk");
Where soapHeader is the string I check. (currently contains username and password(MD5))
How would I send a string from SAVON Rails in the header, so that it can be pulled back off on the webservice.
Preferably without changing the current C# code, unless you can specify a way of sending from a C# client as well.
Ive tried
response = client.request :wsdl, :get_customer_centre_details do
soap.header = { "VerifyUser" => "1:5F4DCC3B5AA765D61D8327DEB882CF99" }
end
Cheers!
EDIT: This is how I currently add the header in C#
OperationContextScope scope = new OperationContextScope(ConChannel);
MessageHeader<String> header = new MessageHeader<String>(UserID + ":" + md5HashString);
var untyped = header.GetUntypedHeader("VerifyUser", "http://www.companyname.co.uk");
OperationContext.Current.OutgoingMessageHeaders.Add(untyped);
What I was missing was the attributes option, to add on the XML namespace, hence it was not being identified as the same header.
response = client.request :wsdl, :get_services do
soap.header = { "VerifyUser" => "1:5F4DCC3B5AA765D61D8327DEB882CF99", :attributes! => { "VerifyUser" => { "xmlns" => "http://www.companyname.co.uk"} } }
end
I have the following code in C# that looks for an apiKey in the the following SOAP header:
SOAP Header:
<soap:Header>
<Authentication>
<apiKey>CCE4FB48-865D-4DCF-A091-6D4511F03B87</apiKey>
</Authentication>
</soap:Header>
C#:
This is what I have so far:
public string GetAPIKey(OperationContext operationContext)
{
string apiKey = null;
// Look at headers on incoming message.
for (int i = 0; i < OperationContext.Current.IncomingMessageHeaders.Count; i++)
{
MessageHeaderInfo h = OperationContext.Current.IncomingMessageHeaders[i];
// For any reference parameters with the correct name.
if (h.Name == "apiKey")
{
// Read the value of that header.
XmlReader xr = OperationContext.Current.IncomingMessageHeaders.GetReaderAtHeader(i);
apiKey = xr.ReadElementContentAsString();
}
}
// Return the API key (if present, null if not).
return apiKey;
}
PROBLEM: Returning null instead of the actual apiKey value:
CCE4FB48-865D-4DCF-A091-6D4511F03B87
UPDATE 1:
I added some logging. It looks like h.Name is in fact "Authentication", which means it won't actually be looking for "apiKey", which then means it won't be able to retrieve the value.
Is there a way to grab the <apiKey /> inside of <Authentication />?
UPDATE 2:
Ended up using the following code:
if (h.Name == "Authentication")
{
// Read the value of that header.
XmlReader xr = OperationContext.Current.IncomingMessageHeaders.GetReaderAtHeader(i);
xr.ReadToDescendant("apiKey");
apiKey = xr.ReadElementContentAsString();
}
I think your h.Name is Authentication because it is root type and apiKey is property of Authentication type. Try logging values of h.Name to some log file and check what does it return.
if (h.Name == "Authentication")
{
// Read the value of that header.
XmlReader xr = OperationContext.Current.IncomingMessageHeaders.GetReaderAtHeader(i);
//apiKey = xr.ReadElementContentAsString();
xr.ReadToFollowing("Authentication");
apiKey = xr.ReadElementContentAsString();
}
Ended up using the following code:
if (h.Name == "Authentication")
{
// Read the value of that header.
XmlReader xr = OperationContext.Current.IncomingMessageHeaders.GetReaderAtHeader(i);
xr.ReadToDescendant("apiKey");
apiKey = xr.ReadElementContentAsString();
}
There is a shorter solution:
public string GetAPIKey(OperationContext operationContext)
{
string apiKey = null;
MessageHeaders headers = OperationContext.Current.RequestContext.RequestMessage.Headers;
// Look at headers on incoming message.
if (headers.FindHeader("apiKey","") > -1)
apiKey = headers.GetHeader<string>(headers.FindHeader("apiKey",""));
// Return the API key (if present, null if not).
return apiKey;
}