views:

377

answers:

3

(warning - unholy mixture of xml and gratuitous character encoding below.)

Short version: Why can't I get my service reference call (c#, .net 3.5, automagic service reference code added to VS2008) to properly encode a parameter that's supposed to look like this one the wire: (look for the " bits...those are my bane.)

(other extra soap-y bits removed for clarity)

<SOAP-ENV:Body><SOAPSDK4:SetCondition xmlns:SOAPSDK4="http://tempuri.org/message/"&gt;
<sharedSecret>buggerall</sharedSecret>

<xmlData>&lt;SEARCHINFO_LIST&gt;&lt;SEARCH_INFO action=&quot;add&quot; status=&
quot;3&quot; name=&quot;TestProfile2&quot; mask=&quot;0&quot; campaign_id=&quot;33&quot; 
campaign_protected=&quot;N&quot; condition_protected=&quot;N&quot;&gt;&lt;CONDITIONS/&
gt;&lt;EXPRESSIONS/&gt;&lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;</xmlData>
</SOAPSDK4:SetCondition></SOAP-ENV:Body>   

I set the parameter, make the call...and the service returns a nice message saying "NO SOU-- SOAP FOR YOU!"

I tried several other formats in my passed-to-the-webservice parameter string:

action=\"add\"

which gave me this on the wire (via fiddler): action="add"

action=&quot;add&quot;

which gave me this on the wire: action=&amp;quot;add&amp;quot;

and various combinations (action=""add"" ?! ) with html.encode, url.encode which pretty much either completely bombed, or showed as double quotes on the wire.

Oh, and I even tried <![CDATA[&quot;]] surrounding. That didn't work either.

Is there any way to force a double quote encoding in the innerHtml bit of the soap message?

*(because that's how the service wants them. don't ask questions. these aren't the droids you're looking for)

* * * Long, tortuous version:

I'm writing an app to automate some procedures that are currently handled by a (winform) administrative GUI app. (Actually, it's an mmc snap in. Anyway.)

To accomplish it's tasks, the winform app communicates with it's server via standard web service calls.

I'm using VS2008's nifty "web service reference" auto generation thingie (that's the technical description), and I've successfully authenticated to the web service. To make sure I was doing things correctly, I captured the calls from the GUI app, and then compared them to what I was sending out on the wire. All was good. Then I ran into the evils of the ampersand. (more properly, how to get things to encode properly)

For one of the calls, the web service expects to see something like this: (I captured the app sending this via fiddler)

<?xml version="1.0" encoding="UTF-8" standalone="no"?><SOAP-ENV:Envelope
xmlns:SOAPSDK1="http://www.w3.org/2001/XMLSchema" xmlns:SOAPSDK2="
http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAPSDK3="
http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="
http://schemas.xmlsoap.org/soap/envelope/"&gt;&lt;SOAP-ENV:Body&gt;&lt;SOAPSDK4:SetCondition
xmlns:SOAPSDK4="http://tempuri.org/message/"&gt;&lt;sharedSecret&gt;0500001007C3525F3-F315-460D-
AF5C-D84767130126094</sharedSecret><xmlData>&lt;SEARCHINFO_LIST&gt;&lt;SEARCH_INFO  
action=&quot;add&quot; status=&quot;3&quot; name=&quot;TestProfile2&quot; mask=&
quot;0&quot; campaign_id=&quot;33&quot;campaign_protected=&quot;N&quot;
condition_protected=&quot;N&quot;&gt;&lt;CONDITIONS/&gt;&lt;EXPRESSIONS/&gt;&
lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;</xmlData></SOAPSDK4:SetCondition></SOAP-
ENV:Body></SOAP-ENV:Envelope>

Stripping out all the extra SOAP-y stuff to show the relevant bit - this is the <xmlData> section that's passed across. Notice the &quot; surrounding the parameters:

&lt;SEARCHINFO_LIST&gt;&lt;SEARCH_INFO action=&quot;add&quot;
status=&quot;3&quot; name=&quot;TestProfile2&quot; mask=&quot;0&quot;
campaign_id=&quot;33&quot; campaign_protected=&quot;N&quot;
condition_protected=&quot;N&quot;&gt;&lt;CONDITIONS/&gt;&lt;EXPRESSIONS/&gt;&
lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;

In my code, I have a string built up like so:

var serviceParams = "<SEARCHINFO-LIST><SEARCH_INFO action=\"add\"
status=\"3\" name=\"TestProfileFromExternApp\" mask=\"0\" campaign_id=\"33\"
campaign_protected=\"N\"
condition_protected=\"N\"><CONDITIONS/><EXPRESSIONS/></SEARCH_INFO></SEARCHINFO_LIST>";

When my app sends it out over the wire, fiddler captures this: (again, stripping out all the SOAP stuff)

&lt;SEARCHINFO-LIST&gt;&lt;SEARCH_INFO action="add" status="3"
name="TestProfileFromExternApp" mask="0" campaign_id="33"
campaign_protected="N" condition_protected="N"&gt;&lt;CONDITIONS/&gt;
&lt;EXPRESSIONS/&gt;&lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;

And the receiving service sends back an error that it doesn't like it. It wants the &quot; d##nit.

The angle brackets get properly encoded, but the quotes are valid in the HTTP string, and do not get encoded.

"Ah-ha!" says I, "I'll just pre-manually encode things!". I attempted to do something like this:

var serviceParams = "<SEARCHINFO-LIST><SEARCH_INFO action=&quot;add&quot;
status=&quot;3&quot; name=&quot;TestProfileFromExternApp&quot;
mask=&quot;0&quot; campaign_id=&quot;33&quot;
campaign_protected=&quot;N&quot;
condition_protected=&quot;N&quot;><CONDITIONS/><EXPRESSIONS/></SEARCH_INFO></SEARCHINFO_LIST>";

Which was sent out as (again, via fiddler) and all my ampersands (in the &quot;) get converted to &amp;quot; like so:

&lt;SEARCHINFO-LIST&gt;&lt;SEARCH_INFO action=&amp;quot;add&amp;quot;
status=&amp;quot;3&amp;quot;
name=&amp;quot;TestProfileFromExternApp&amp;quot; mask=&amp;quot;0&amp;quot;
campaign_id=&amp;quot;33&amp;quot; campaign_protected=&amp;quot;N&amp;quot;
condition_protected=&amp;quot;N&amp;quot;&gt;&lt;CONDITIONS/&gt;&lt;EXPRESSIONS/&gt;&lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;

And, as you can guess, the receiving web service came back with "BZZT! Thank you for playing!".

I've tried all sorts of escape and encode sequences with similar results. Effectively, after all my manipulation it goes through something like HttpUtility.HtmlEncode right before going out on the wire, and any ampersands in the string get converted to &amp;. And quotes (single or double) are ignored in the conversion. And the receiving web service wants those quotes represented as &quot; doggone it, or it's going to take it's ball and go home.

My last, desperate hope was to catch the message right (I thought) before it went onto the wire using IClientMessageInspector to implement message inspection in the BeforeSendRequest event...and manually set those things before it went to the wire.

I capture the message fine. I can even manually put in &quot;.

But when it gets sent, both wireshark and fiddler assure me it's going out nicely formatted...with the quotes I'm so desperately trying to get rid of.

<xmlData xsi:type="xsd:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&gt;
&lt;SEARCHINFO-LIST&gt;&lt;SEARCH_INFO action="add" status="3" 
name="TestProfileFromExternApp" mask="0" campaign_id="33" campaign_protected="N" 
condition_protected="N"&gt;&lt;CONDITIONS/&gt;&lt;EXPRESSIONS/&gt;
&lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;</xmlData>

I'm at my wits end. I'll accept any suggestions up to and including sacrificing [small cuddly thing] on the alter of [vile deity] or selling same my [soul/heart/son's bionacle collection]. Trust me, it'd be the lesser evil.

thanks!! -aaron

Per request below, here's the generated message stub: (I think this is what you were asking for...)

public int SetCondition(string sharedSecret, string xmlData, out string resultValue) 
{
    tzGui.tzCampaign.SetConditionRequest inValue = new tzGui.tzCampaign.SetConditionRequest();
    inValue.sharedSecret = sharedSecret;
    inValue.xmlData = xmlData;
    tzGui.tzCampaign.SetConditionResponse retVal = ((tzGui.tzCampaign.CampaignSoapPort)(this)).SetCondition(inValue);
    resultValue = retVal.resultValue;
    return retVal.Result;
}

And here's how it's getting called:

void SetConditionTask()
{
    //ok, now we *try* and create a new profile
    var tzCampaignCxn = new tzCampaign.CampaignSoapPortClient("CampaignSoapBinding");
    //no worky
    //string xmlData = "<SEARCHINFO-LIST><SEARCH_INFO action=\"add\" status=\"3\" name=\"TestProfileFromExternApp\" mask=\"0\" campaign_id=\"33\" campaign_protected=\"N\" condition_protected=\"N\"><CONDITIONS/><EXPRESSIONS/></SEARCH_INFO></SEARCHINFO_LIST>";

    //this one doesn't work
    //string xmlData = "<SEARCHINFO-LIST><SEARCH_INFO action=<![CDATA[ &quot; ]]>add<![CDATA[ &quot; ]]> status=<![CDATA[ &quot; ]]>3<![CDATA[ &quot; ]]> name=<![CDATA[ &quot; ]]>TestProfileFromExternApp<![CDATA[ &quot; ]]> mask=<![CDATA[ &quot; ]]>0<![CDATA[ &quot; ]]> campaign_id=<![CDATA[ &quot; ]]>33<![CDATA[ &quot; ]]> campaign_protected=<![CDATA[ &quot; ]]>N<![CDATA[ &quot; ]]> condition_protected=<![CDATA[ &quot; ]]>N<![CDATA[ &quot; ]]>><CONDITIONS/><EXPRESSIONS/></SEARCH_INFO></SEARCHINFO_LIST>";

    //this one doesn't either
    string xmlData = "<SEARCHINFO-LIST><SEARCH_INFO action=&quot;add&quot; status=&quot;3&quot; name=&quot;TestProfileFromExternApp&quot; mask=&quot;0&quot; campaign_id=&quot;33&quot; campaign_protected=&quot;N&quot; condition_protected=&quot;N&quot;><CONDITIONS/><EXPRESSIONS/></SEARCH_INFO></SEARCHINFO_LIST>";

    string createProfileResultVal = string.Empty;
    tzCampaignCxn.SetCondition(SharedSecret, xmlData, out createProfileResultVal);
    txtResults.AppendText(Environment.NewLine + Environment.NewLine + createProfileResultVal);
    }
A: 

You need to get that string into the SOAP message as a CDATA section, so that you can format it exactly as you want, and so that WCF doesn't touch it.

The problem you have at the moment is that the service is expecting an encoding that somewhere between xml and html - which is therefore not nicely handled by format that the Soap formatter is using - so you are always going to have to escape the quotes yourself.

I have a hunch, though, that if you surround it in the CDATA tags <![CDATA[*yourstring*]]> in your string parameter, the markup for that will be escaped over the wire and reach the other end as &lt;![[*yourstring-XML-element-encoded*]]&gt;

If that's the case, what you could do is to alter the code that was generated automatically by the VS service reference generator so that the parameter type of the method call on the proxy accepts a type that is basically the same as a string, but serializes itself as a CDATA section. Such a type is provided based on code written by Marc Gravell to answer another question here. The problem with doing this will be that if anyone uses the 'Update Reference' command in VS on that service reference, any changes you make will be lost.

So, instead, generate the reference with the svcutil command line utility (contrary to popular belief, VS doesn't use this tool - which is a shame because it is more flexible), import the config and code it generates into your project manually and get rid of the 'service reference'. That way, the code is yours to hack however you want, and you can easily see it in the project tree in VS.

Andras Zoltan
I did try surrounding the quote `"` with the cdata bit. Unfortunately, even that wasn't respected. Here's what went out on the wire:`<SEARCHINFO-LIST><SEARCH_INFO action=<``![CDATA[]]>add<![CDATA[ ]]>``status=<![CDATA[ ]]>3<![CDATA[ ]]>``name=<![CDATA[ ]]>TestProfileFromExternApp<``[CDATA[ ]]> mask=<![CDATA[ `<snip..you get the idea>I just can't win! :)
Aaron B.
I meant the whole string - not just around the quotes - but either way as I suspected, it's still formatting out the characters.In which case you could try using the CDataWrapper class from the other example I linked to, which implements IXmlSerializable explicitly so that it sends a string out as CDATA - by doing this you prevent all of the default WCF message formatting for that parameter's data - it's just that it needs to reach the WCF formatter as the CDataWrapper class, not a string - hence the need to alter the proxy code that has been generated for you.
Andras Zoltan
Mmm...now that looks *exactly* like what I need. I'll give it a try...
Aaron B.
Tried changing all the references to string xmlData to CDataWrapper and it looked like it was going to go, but then:{"There was an error reflecting 'xmlData'."}"The type tzGui.CDataWrapper may not be serialized with SOAP-encoded messages. Set the Use for your message to Literal."I changed the use=encoded to use=literal in the Campaign.WSDL (like so):***<wsdl:input><soap:body use="literal" namespace="http://tempuri.org/message/" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" parts="sharedSecret xmlData" /></wsdl:input> *** Same error. Maybe I didn't change it in the correct spot?
Aaron B.
I think you did - unless the WSDL should have been changed to reflect the fact that CDataWrapper type is now expected for that parameter instead of string.The question is, is it a facet of how the string itself is being serialized, or is it the message formatter through which this data is writing - if it's the latter, then we could swap types around however much we want, but it would still do the damned escaping, or non-escaping, depending upon which way you look at it ;). I'll try and lab up a service and client when I get a chance to see if I can reproduce and help further.
Andras Zoltan
+1  A: 

If I understand your long post, you seem to be building parts of your XML by using strings. Don't do that. Always use one of the XML APIs to create XML. It knows the quoting rules.

John Saunders
A: 

I never did fully figure out how to get this to work. However, I did find a solution. An ugly, hackish solution that I'm not proud of. But it worked.

In the interest of posterity, here's what I finally did. For each of the (10ish) calls I needed to make, I simply captured the SOAP call via fiddler, and then wrote a quick search-n-replace routine

    public string Newrule(string ruleName, DecisionSet decisionSet)
    {
        var soapString = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><SOAP-ENV:Envelope " +
                         "xmlns:SOAPSDK1=\"http://www.w3.org/2001/XMLSchema\" xmlns:SOAPSDK2=\"http://www.w3.org/2001/XMLSchema-instance\" " +
                         "xmlns:SOAPSDK3=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"&gt;" +
                         "<SOAP-ENV:Body><SOAPSDK4:SetCondition xmlns:SOAPSDK4=\"http://tempuri.org/message/\"&gt;&lt;sharedSecret&gt;&lt;/sharedSecret&gt;" +
                         "<xmlData>&lt;SEARCHINFO_LIST&gt;&lt;SEARCH_INFO action=&quot;add&quot; status=&quot;3&quot; " +
                         "name=&quot;" + ruleName + "&quot; mask=&quot;0&quot; DecisionSet_id=&quot;" + decisionSet.Id +
                         "&quot; DecisionSet_protected=&quot;N&quot; " +
                         "condition_protected=&quot;N&quot;&gt;&lt;CONDITIONS/&gt;&lt;EXPRESSIONS/&gt;&lt;/SEARCH_INFO&gt;&lt;/SEARCHINFO_LIST&gt;" +
                         "</xmlData></SOAPSDK4:SetCondition></SOAP-ENV:Body></SOAP-ENV:Envelope>";

        var headerUrl = "http://tempuri.org/action/DecisionSet.SetCondition";
        var serviceUrl = "/webservice/DecisionSet.WSDL";
        var result = sender.MakeRequest(soapString, serviceUrl, headerUrl,null);
        var idSearch = @"SEARCH_INFO id=&quot;(\d+)&quot;";

        var ruleId = Regex.Match(result, idSearch).Groups[1].Value;

        return ruleId;
    }

This called a simple routine to make an http call with the appropriate headers. Inelegant, but it worked.

    public string MakeRequest(string requestString, string serviceUrl, string headerUrl, string useragent)
    {
        string query = requestString.Replace(@"<sharedSecret></sharedSecret>", "<sharedSecret>"+secret+"</sharedSecret>");
        query = query.Replace(@"<SessionID></SessionID>", "<SessionID>" + secret + "</SessionID>");
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(server + serviceUrl);
        //if (proxy != null) req.Proxy = new WebProxy(proxy, true);
        req.Headers.Add("SOAPAction", headerUrl);
        if (useragent == null)
            req.UserAgent = "SOAP Toolkit 3.0";
        else
        {
            req.UserAgent = useragent;
        }
        req.ContentType = "text/xml;charset=\"utf-8\"";
        req.Accept = "text/xml";
        req.Method = "POST";
        Stream stm = req.GetRequestStream();

        StreamWriter sw = new StreamWriter(stm);
        sw.Write(query);
        sw.Flush();
        stm.Close();
        WebResponse resp = req.GetResponse();
        stm = resp.GetResponseStream();
        StreamReader r = new StreamReader(stm);
        string response = (r.ReadToEnd());

        return response;
    }
Aaron B.