Tuesday, November 18, 2008

Mac To WCF WebService - Passing Parameters Step 2

In the last post I'd reached the stage of being able to call a WCF service and receive a response, however had the problem that my iPhone application SOAP request has slightly out of alignment.
To recap my application expects:
<s:Body>
<Generate xmlns="http://tempuri.org/">
<noun1>hello</noun1>
<noun2>hello</noun2>
</Generate>
</s:Body>

While the iPhone application is sending:
<SOAP-ENV:Body>
<Generate xmlns=\"http://tempuri.org/\">
<parameters xsi:type=\"xsd:string\">hello</parameters>
</Generate>
</SOAP-ENV:Body>

Looking within the JokeGen.m file shows how the XML for the parameters are formed:
- (void) setParameters:(CFTypeRef) in_parameters
{
id _paramValues[] = {
in_parameters,
};
NSString* _paramNames[] = {
@"parameters",
};
[super setParameters:1 values: _paramValues names: _paramNames];
}

However as this is using a NSDictionary pointer to hold the information passed to the object base in WSGeneratedObj.m (shown below) we loose the ability to manipulate this information easily. Converting to a NSMutableDictionary would be the obvious answer, however this would mean changing the base generated code and really did not seem like the right approach:

- (void) setParameters:(int) count values:(id*) values names:(NSString**) names
{
NSDictionary* params = [NSDictionary dictionaryWithObjects:values forKeys: names count: count];
NSArray* paramOrder = [NSArray arrayWithObjects: names count: count];
WSMethodInvocationSetParameters([self getRef], (CFDictionaryRef) params, (CFArrayRef) paramOrder);
}


So how to get round this little issue? Well, what we need is a workable model or pattern that could be used over and over again and cope with the inevitable changes that would be required long term as the WCF is updated with additional fields or methods. So I chose to revert to an old fashioned WebContract pattern which has served me well over the years. This would unfortunately require a bit of re-factoring of both the iPhone application and the WCF Service, however the impact should be minimal.

First thing we need to do is to minimize as much of the hard coding that currently exists. Without the use of reflection in the SDK I can't use any of my usual short cuts however there are a few tricks available. We need to create a defined contract between the services which I'm calling WSJokeRequest. Create this in XCode using File/New File and select "NSObject subclass" and click Next. Enter the file name "WSJokeRequest" making sure that "Also create "WSJokeRequest.h" is ticked and click Finish

Edit WSJokeRequest.h and enter the following code:

@interface WSJokeRequest : NSObject {
NSMutableDictionary *resultDictionary;
}

-(id)initWithDictionary:(NSDictionary *)dictionary;
-(NSDictionary *)dictionary;

-(void)setNoun1:(NSString *)noun1;
-(NSString *)noun2;
-(void)setNoun2:(NSString *)noun2;
-(NSString *)noun1;

@end


We are going to create a defined object which holds all the required keys on initialization and then provide methods to alter these keys any time we need, i.e. simple get/set methods Object C style.

Edit WSJokeRequest.m and enter the following code:

@implementation WSJokeRequest
-(id)init
{
return [self initWithDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"noun1",
@"", @"noun2",
nil]];
}

-(id)initWithDictionary:(NSDictionary *)dictionary
{
self = [super init];
resultDictionary = [[dictionary mutableCopy] retain];
[resultDictionary setObject:@"http://tempuri.org/" forKey:(NSString *)kWSRecordNamespaceURI];
[resultDictionary setObject:@"WSJokeRequest" forKey:(NSString *)kWSRecordType];
return self;
}

-(void)dealloc
{
[resultDictionary release];
[super dealloc];
}

-(NSDictionary *)dictionary
{
return resultDictionary;
}

-(NSString *)noun1
{
NSString *value = [resultDictionary objectForKey:@"noun1"];
return value;
}

-(void)setNoun1:(NSString *)Noun1
{
[resultDictionary setObject:Noun1 forKey:@"noun1"];
}

-(NSString *)noun2
{
NSString *value = [resultDictionary objectForKey:@"noun2"];
return value;
}

-(void)setNoun2:(NSString *)Noun2
{
[resultDictionary setObject:Noun2 forKey:@"noun2"];
}
@end


Now we open the View Controller code (iWCFDemoViewController.m) and re-factor the pingTest method to make use of this new contract.

-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed!!");
WSJokeRequest *wsJokeRequest = [[WSJokeRequest alloc] init];
[wsJokeRequest setNoun1:@"hello"];
[wsJokeRequest setNoun2:@"there"];
NSString *result = [JokeService Generate:[wsJokeRequest dictionary]];
NSLog([result description]);
resultsField.text = [result objectForKey: @"GenerateResult"];
}


The last step is to change the JokeGen.m to use the name of the new Contract object:
- (void) setParameters:(CFTypeRef) in_parameters
{
id _paramValues[] = {
in_parameters,
};
NSString* _paramNames[] = {
@"wsJokeRequest",
};
[super setParameters:1 values: _paramValues names: _paramNames];
}


Running the application in debug mode now produces a well formed SOAP request in the form we can work with:
<s:Body>
<Generate xmlns="http://tempuri.org/">
<wsJokeRequest xsi:type="WSJokeRequest">
<noun1>hello</noun1>
<noun2>there</noun2>
</wsJokeRequest>
</Generate>
</s:Body>

Now it's time to re-factor the WCF side of the contract which currently still expects two string 'noun1' and 'noun2'.

First we add a new class to the WebService in Visual Studio called WSJokeReqest by right clicking on the APP_Code folder and selecting Add New Item. From the dialogue box select Class and enter WSJokeReqest.cs into the name field. When the file appears enter the following code:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

///
/// Summary description for WSJokeRequest
///

[DataContract(Namespace="http://tempri.org/")]
public class WSJokeRequest
{
public string noun1 { get; set; }
public string noun2 { get; set; }
}


Now we change the WCF Interface (IJoke,cs) and Class (Joke.cs) files to make use of this as a parameter into the Generate function.
In iJoke.cs the contract changes from:
string Generate(string noun1, string noun2);
to
[OperationContract]
string Generate(WSJokeRequest wsJokeRequest);

In Joke.cs the Generate method changes from:

public string Generate(string noun1, string noun2)
{
return String.Format("What do you call an Irish {0}?\n A: An {1}", noun1, noun2);
}

to

public string Generate(WSJokeRequest wsJokeRequest)
{
return String.Format("What do you call an Irish {0}?\n A: An {1}", wsJokeRequest.noun1, wsJokeRequest.noun2);
}

The application should now build successfully without any errors.

Finally we need to update our test harness application which was created to verify that our XML request is being formed correctly. If you followed the instructions in the previous post you should have a JokeGenTestHarness project already included in the current solution. If not you should open in Visual Studio that now. Right click the Service reference called JokeGen within the ServiceReferences folder and select "Update Service Reference". Once this in complete we edit the Form.cs code by right clicking the file and selecting "View code". Change the click method on the form to

private void btnPress_Click(object sender, EventArgs e)
{
using (JokeClient wcfJokeGen = new JokeClient())
{
WSJokeRequest WSJokeRequest = new WSJokeRequest();
WSJokeRequest.noun1 = txtEntry.Text;
WSJokeRequest.noun2 = txtEntry.Text;
txtResult.Text = wcfJokeGen.Generate(WSJokeRequest);
}
}



Recompile the application and start the Webservice and test harness in debug mode. Entering the value "Hello" into the Noun1 field in the test harness should return "What do you call an Irish Hello? A: An Hello" in the results field.



Checking the Service Trace Viewer shows that we are now sending the following XML:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:8888/JokeGen/Joke.svc</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IJoke/Generate</Action>
</s:Header>
<s:Body>
<Generate xmlns="http://tempuri.org/">
<wsJokeRequest xmlns:a="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:noun1>Hello</a:noun1>
<a:noun2>Hello</a:noun2>
</wsJokeRequest>
</Generate>
</s:Body>
</s:Envelope>

Woo Hoo!! All systems Go.... ! Now we try the iPhone application running in debug mode.

Unfortunately when we run the application we get the error:
"/FaultCode" = -1;
"/FaultString" = "The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:wsJokeRequest. The InnerException message was 'Error in line 10 position 79. Element 'http://tempuri.org/:wsJokeRequest' contains data of the 'http://tempuri.org/:WSJokeRequest' data contract. The deserializer has no knowledge of any type that maps to this contract. Add the type corresponding to 'WSJokeRequest' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.";

Luckily I'd run across this problem before in WCF. reason for this is that by default the deserializer is using DataContractSeriazer which is fine for Microsoft native applications but it won't play fair with other connections. To address this we decorate the interface file IJoke.cs with the XmlSerializerFormat attribute like so and all should be well:
[XmlSerializerFormat]
[ServiceContract(Namespace="http://tempuri.org/")]
public interface IJoke

Running the iPhone application now correctly takes in the values and returns the expected string.



Next iteration of this application will be local storage and retrieval of data.

Friday, November 14, 2008

Mac To WCF WebService - Passing Parameters

In my last posting I showed the process of creating a iPhone application which can be used to call a WCF WebService and return a valid result. The next stage in the evolution of this application is to modify the code to send values to the service which acts on those values and returns specific results.

Working on this I figured would be a simple matter of adding some structure to the generated proxy classes and away we go, but sadly Apple again let us down on the really bad implementation of the data structures. This of course leads me to describe the many. many extra functions you will need to do to get this element to work.

First lets review our WCF WebService, which was defined in my previous article, we have two simple public methods; "bool Ping()" and "string Generate(string noun1, string noun2)". I've already shown how to connect to the first, now lets focus on the second. Generate takes two string parameters, then concatenates these within a Joke which is returned to the User. On the Objective C side of the equation I was simply going to change the calling code in the pingTest method from:

-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed: ");
NSString *result = [[JokeGen Ping:@""] objectForKey:@"/Result"];
NSLog([result description]);
resultsField.text = [result objectForKey: @"PingResult"];
}

To
-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed: ");
NSString *result = [[JokeGen Genereate:@"hello"] objectForKey:@"/Result"];
NSLog([result description]);
resultsField.text = [result objectForKey: @"GenerateResult"];
}
-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed: ");
NSString *result = [[JokeGen Ping:@""] objectForKey:@"/Result"];
NSLog([result description]);
resultsField.text = [result objectForKey: @"GenerateResult"];
}


The rational being that all all I was doing was changing the parameters which went into the SOAP request and therefore everything should just work. Oh how wrong can one person be ..... :-) Make this change in the code worked fine for the service call, I got my string back from the WebService, but no parameters had been passed. Setting a break point with Visual Studio confirmed that the two values for noun1 and noun2 where null.

Luckily as part of my investigations I'd learned quite a bit about debugging the SOAP requests between both applications. Using svctraceviewer which I described previously seemed to give me some of the information but sadly not the actual XML messages sent and received. What I needed to do was to obtain the actual SOAP request being sent via the iPhone application and compare that to a working SOAP request from a .Net Client application.

First I reviewed the XML being sent from the iPhone in the Debug screen in XCode, I'd already managed to do that using extra settings in the Generate stub which is described in more detail here. This tells me that I was sending the following:

"/WSDebugInBody" = "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><ActivityId CorrelationId=\"ecb10aea-3e99-4472-bed1-0100aecb517e\" xmlns=\"http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics\">574f4c95-a28f-4a8b-ab6e-8ad0f16152c4</ActivityId></s:Header><s:Body><GenerateResponse xmlns=\"http://tempuri.org/\"><GenerateResult>What do you call an Irish ?\n A: An</GenerateResult></GenerateResponse></s:Body></s:Envelope>";
"/WSDebugInHeaders" = {
"Cache-Control" = private;
Connection = close;
"Content-Length" = 434;
"Content-Type" = "text/xml; charset=utf-8";
Date = "Thu, 04 Dec 2008 18:59:03 GMT";
Microsoftofficewebserver = "5.0_Pub";
Server = "Microsoft-IIS/6.0";
"X-Aspnet-Version" = "2.0.50727";
"X-Powered-By" = "ASP.NET";
};
"/WSDebugOutBody" = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n <SOAP-ENV:Envelope\n\n xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\n xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\"\n\n SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"\n\n xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n\n <SOAP-ENV:Body>\n\n <Generate xmlns=\"http://tempuri.org/\">\n\n <parameters xsi:type=\"xsd:string\">hello</parameters>\n\n </Generate>\n\n </SOAP-ENV:Body>\n\n </SOAP-ENV:Envelope>\n\n";
"/WSDebugOutHeaders" = {
"Content-Type" = "text/xml";
Host = "192.168.1.106";
Soapaction = "http://tempuri.org/IJoke/Generate";
"User-Agent" = "Mac OS X; WebServicesCore.framework (1.0.0)";
};
"/WSHTTPMessageHeaders" = (
<NSCFType: 0x556730>
);
"/kWSHTTPResponseMessage" = <NSCFType: 0x543020>;
GenerateResult = "What do you call an Irish ?\n A: An";
}

Next I had to increase the logging for the Service and to do this I had to make additional changes to the services web.config file.

<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.ServiceModel.MessageLogging" switchValue="Verbose,ActivityTracing">
<listeners>
<add type="System.Diagnostics.DefaultTraceListener" name="Default">
<filter type="" />
</add>
<add name="ServiceModelMessageLoggingListener">
<filter type="" />
</add>
</listeners>
</source>
<source name="System.ServiceModel"
switchValue="Information, Verbose, ActivityTracing"
propagateActivity="true">
<listeners>
<add type="System.Diagnostics.DefaultTraceListener" name="Default">
<filter type="" />
</add>
<add name="ServiceModelTraceListener">
<filter type="" />
</add>
<add name="sdt"
type="System.Diagnostics.XmlWriterTraceListener"
initializeData= "SdrConfigExample.e2e" />
</listeners>
</source>
</sources>
<sharedListeners>
<!-- This is where the messages will be logged -->
<add initializeData="Web_messages.svclog"
type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="ServiceModelMessageLoggingListener" traceOutputOptions="Timestamp">
<filter type="" />
</add>
<add initializeData="Web_tracelog.svclog"
type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="ServiceModelTraceListener" traceOutputOptions="Timestamp">
<filter type="" />
</add>
</sharedListeners>
</system.diagnostics>

And adding to the existing system.serviceModel tags..

<diagnostics wmiProviderEnabled="true">
<messageLogging logEntireMessage="true" logMalformedMessages="true"
logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" />
</diagnostics>

The above should produced two extra files Web_messages.svclog and Web_tracelog.svclog which can be opened by the the svctraceviewer application. The resulting values looked like the following, with the right panel showing the actual XML being passed e.g.:


Now all I needed to do compare the SOAL XML from the iPhone to a working version, so I created a small Client application in Visual Studio 2008 added the WebReference to my JokeGen application and executed the call. The code for this was very simple:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using JokeGenTestHarness.JokeGen;

namespace JokeGenTestHarness
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void btnPress_Click(object sender, EventArgs e)
{
using (JokeClient wcfJokeGen = new JokeClient())
{
txtResult.Text = wcfJokeGen.Generate(txtEntry.Text, txtEntry.Text);
}
}
}
}

with a simple WinForm which looked like this:



The results of this showed that there was a slight difference in the way the Body tag was being formed.
iPhone:
<SOAP-ENV:Body>
<Generate xmlns=\"http://tempuri.org/\">
<parameters xsi:type=\"xsd:string\">hello</parameters>
</Generate>
</SOAP-ENV:Body>

Windows Test App, assuming all the NameSpace and Header information is correct:
<s:Body>
<Generate xmlns="http://tempuri.org/">
<noun1>hello</noun1>
<noun2>hello</noun2>
</Generate>
</s:Body>

OK.. Now we are getting somewhere! All thats needed is for the parameters tag to be changed to a <noun1> and <noun2> and it should just work. How to do this will have to wait until my next posting.......

Saturday, November 8, 2008

Tracing WCF messages on a Windows Server

If you use WCF for communication, you'll need to do quite a bit of debugging if you ever want to get to the bottom of serialization issues. Microsoft has thankfully provided a responsible tracing application which is installed with the SDK.

The Service Trace Viewer tool reads the default log file produced from the Server. To get this to be produced by your WCF Server simply add the following section to your web.config file.




switchValue="Information, ActivityTracing"
propagateActivity="true">

type="System.Diagnostics.XmlWriterTraceListener"
initializeData= "SdrConfigExample.e2e" />






This will produce a file in the root directory called SdrConfigExample.e2e. Start the Trace Viewer by clicking on the SvcTraceViewer.exe which is probably located in the default installation area C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin. Click File/Open and select the SdrConfigExample.e2e file, you'll probably have to select the "All Files (*.*)" option.
As soon as this is opened you should see all the messages which have been sent.

Tuesday, November 4, 2008

Adding Additional Debugging to a WCF call

I've recently been having a number of problems sending complex data types to a WCF service from an iPhone application I've been writing. I found this posting on the Apple site which increases the amount of debug information sent on each call.

// set debug props
WSMethodInvocationSetProperty(soapReq, kWSDebugIncomingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(soapReq, kWSDebugIncomingHeaders, kCFBooleanTrue);
WSMethodInvocationSetProperty(soapReq, kWSDebugOutgoingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(soapReq, kWSDebugOutgoingHeaders, kCFBooleanTrue);

Adding this to the genCreateInvocationRef method in your generated code from WSMakeStub did not require a lot of effort. I'd probably recommend it for each call.

e.g.

- (WSMethodInvocationRef) genCreateInvocationRef
{
WSMethodInvocationRef ref = [self createInvocationRef
/*endpoint*/: @"[URL]"
methodName: @"Generate"
protocol: (NSString*) kWSSOAP2001Protocol
style: (NSString*) kWSSOAPStyleDoc
soapAction: @"[method]"
methodNamespace: @"[namespace]"
];

WSMethodInvocationSetProperty(ref, kWSDebugIncomingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(ref, kWSDebugIncomingHeaders, kCFBooleanTrue);
WSMethodInvocationSetProperty(ref, kWSDebugOutgoingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(ref, kWSDebugOutgoingHeaders, kCFBooleanTrue);
return ref;

}

Creating Local DNS entries on the Leopard

I recently had a problem with a WCF service which would not resolve correctly on my Mac. It turned out that although I was accessing the Service using an IP address the WSDL XML file contained a number of references to the host name. To address this I needed to create a local DNS lookup so it could resolve correctly.


In Windows this is a simple matter of editing the Hosts file in the C:\WINDOWS\system32\drivers\etc\hosts file.

For the Mac OSX we use the command:
sudo dscl localhost -create /Local/Default/Hosts/myHost IPAddress [IPAddress], then Enter the administrator password. Where myHost is the name of the host and IPAddess the address to resolve too.
Removing the entry is a matter of entering:
sudo dscl localhost -delete /Local/Default/Hosts/myHost