|
|
Debugging tips and small examples for WCF
-
When Silverlight 2 was launched, creating SL apps which handled faults thrown by WCF services was at least a little cumbesome. According to the instructions listed at http://msdn.microsoft.com/en-us/library/dd470096(VS.95).aspx, one needed to have a special behavior at the server to change the fault HTTP response status code, which is normally 500 (Internal Server Error) to 200 (OK), to overcome the limitation of the networking stack in which the HTTP response body could only be read in 200 responses. This was cumbersome for a variety of reasons: the behavior didn't apply to all faults (including those related to authentication), an extra piece of data was required at the service side (and if you don't have control of the service this is not an option).
With Silverlight 3, there is now an option to use a different networking stack (the client networking stack), which bypasses the browser HTTP stack and uses the native OS stack directly. By using that stack, the SL app is now able to read the responses to any HTTP request (including those with status code other than 200, like the ones normally used in faults). The full differences between the new (client) and the "old" (browser) stacks are nicely listed at http://blogs.msdn.com/silverlight_sdk/archive/2009/08/12/new-networking-stack-in-silverlight-3.aspx.
So, what does it mean for consuming faults from WCF (or not) services? As long as you have the following line in the beginning of the SL application (e.g., on the constructor for the MainPage class), the app will be using the new stack for all communication to http:// services:
bool registerResult = WebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
If the registration returns true (which it should, unless the prefix has been previously registered), then you now don't need any special behaviors on the WCF service - all faults should be received correcly by the SL application.
|
-
Recently I had to implement an asynchronous operation which would in turn call another asynchronous operation (i.e., cascading async operations). For simple cases, when there's no intermediate processing in the middle of the chains (i.e., no new state that needs to be passed around), this is fairly straightforward (or as straightforward as async programming can be):
[ ServiceContract] public interface ITest { [OperationContract(AsyncPattern = true)] IAsyncResult BeginEchoString(string input, AsyncCallback callback, object userState); string EndEchoString(IAsyncResult asyncResult); } public class Service : ITest { string EchoStringDoWork(string input) { return input; } public IAsyncResult BeginEchoString(string input, AsyncCallback callback, object userState) { Func<string, string> func = EchoStringDoWork; return func.BeginInvoke(input, callback, userState); } public string EndEchoString(IAsyncResult asyncResult) { System.Runtime.Remoting.Messaging.AsyncResult typedAsyncResult = (System.Runtime.Remoting.Messaging.AsyncResult)asyncResult; Func<string, string> func = (Func<string, string>)typedAsyncResult.AsyncDelegate; return func.EndInvoke(asyncResult); } }
This can be shown in the image at http://cid-99984bbbec66d789.skydrive.live.com/self.aspx/Public/Blog/Images/SimpleChainedAPM.gif (whenever I find out how to embed the image here I'll update this post).
When I needed to pass around some extra information, however, I found out that it wasn't as easy as before, and since I couldn't bing (or google, yes, I also tried there) any information about that, I decided to post my solution here. I really hope this isn't the "best" way to do it, and that I simply didn't search well enough, as I find it a lot harder than it should be.
Basic Async Programming Model
First some basic rules for the Begin/End async programming model (APM). There's also an event-based model which is a lot simpler than the Begin/End one, but in many cases it's not available, so I'll not discuss it here. A user makes a BeginXXX call into a library, passing a callback (System.AsyncCallback delegate) along with some additional parameters. When the library work is done, it will invoke the callback delegate, notifying the user that the results are available. Finally, the user will call EndXXX to fetch the results from the operation.
Notice that there are cases when one can make synchronous calls to Begin/End operations (either by waiting on the WaitHandle, http://msdn.microsoft.com/en-us/library/ms228962.aspx, or by calling EndXXX before the callback is invoked, http://msdn.microsoft.com/en-us/library/ms228967.aspx), but this defeats the purpose of the APM (be able to do other tasks while the operation is being executed), and in some cases it's not valid to do so (like in Silverlight), so again I'll not get into those cases.
The main (and only) point where a user can pass some state along asynchronous calls is the last parameter of the operation, usually called userState or asyncState, of type System.Object. This state object has to be returned to the user all the time: both in the return value of the BeginXXX call (typed IAsyncResult) and in the IAsyncResult (IAR) parameter passed to the callback specified by the user. If you need some extra information to be passed along the state to the next operation in the chain, however, the code starts getting a lot more complex.
Chaining async calls with additional information
The first problem with the simple approach for additional state: if you don't pass the userState parameter from the user, you can't return the same IAsyncResult received from the call to BeginOp2 in BeginOp1. You'll essentially need to create a new implementation of the IAsyncResult interface to return to the user, so that they can access their AsyncState property from the resulting IAsyncResult object. That's already an extra class needed here.
The second problem is that, since you passed a different userState to BeginOp2, you cannot pass the callback the user supplied, otherwise it would be called with your state, not theirs. And then we need an intermediate callback method to receive the notification that Op2 is done, and only then we can call the user callback, passing their state to them.
Now, when the user receives the callback, they will call EndOp1 passing only IAsyncResult parameter (the same one you passed to them in the call to callback1. On EndOp1, you will need to return the result of the method, so any information necessary to do so must be contained in the IAsyncResult itself (and the "only" place that can be used to store additional information, the AsyncState property, is already taken by the user's userState).
Confusing? I thought so too. Hopefully the diagram at http://cid-99984bbbec66d789.skydrive.live.com/self.aspx/Public/Blog/Images/ComplexChainedAPM.gif will give a little clarity to the scenario.
My (more complex than I'd like) solution
The first thing that needed to be addressed was the need for a new IAsyncResult implementation. The easiest way to do that was to wrap the IAR returned by BeginOp2 into another IAR which could contain additional information. All properties are delegated to the inner IAR, except the AsyncState, which should be the user provided one. To make managing the APM data easier, I forced the my state to contain both the user callback and the user state, this way I could write a generic IAR implementation and reuse it in different locations. On BeginOp1, I create this new IAR implementation and return it to the user.
The next step is the callback (i.e., "callback2"), which receives the state with all the user information (callback delegate, user state), so I could simply recreate the IAR to send it back to the user callback (i.e., "callback1"). When I receive my callback I already call EndOp2, and store all the result information in my state (another option would be to defer calling EndOp2 until the user calls EndOp1, in which case the state didn't need to store the result information, but it'd need to store the IAR passed to the callback to be able to call EndOp2 at that point - see diagram at http://cid-99984bbbec66d789.skydrive.live.com/self.aspx/Public/Blog/Images/ComplexChainedAPM2.gif for this scenario).
Finally, on the EndOp1 implementation I used the fact that the user must call EndOp1 with the same IAR that is passed to its callback, so I can cast it to my own class and retrieve the state from there. Since the state already had all the information for the result, the job is done.
And here is my implementation of a WCF operation using the async pattern, which calls another async operation (HttpWebRequest.BeginGetResponse). We need the HttpWebRequest instance to be able to call EndGetResponse after the callback is invoked, so I needed all the complexities required by the additional state (this isn't the best example, as HttpWebRequest has a synchronous GetResponse method, but it's simple enough to be used in this post).
// Chaining async operations public class BlogPost { [ServiceContract] public interface ITest { [OperationContract(AsyncPattern = true)] IAsyncResult BeginProcess(string text, AsyncCallback callback, object userState); string EndProcess(IAsyncResult asyncResult); } [ServiceContract(Name = "ITest")] public interface ISyncTest { [OperationContract] string Process(string text); } public class Service : ITest { internal class MyState : MyUserStateBase { public MyState(AsyncCallback userCallback, object userState) : base(userCallback, userState) { } public HttpWebRequest req; public string input; public string GETResponse; } public IAsyncResult BeginProcess(string text, AsyncCallback callback, object userState) { Console.WriteLine("[server] Inside BeginProcess"); Uri baseAddress = OperationContext.Current.Host.BaseAddresses[0]; HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress); req.Method = "GET"; MyState myState = new MyState(callback, userState) { input = text, req = req }; Console.WriteLine("[server] Inside BeginProcess, called HttpWebRequest.BeginGetResponse"); IAsyncResult asyncResult = req.BeginGetResponse(new AsyncCallback(HttpGetCallback), myState); return new MyAsyncResult<MyState>(asyncResult, myState); } void HttpGetCallback(IAsyncResult asyncResult) { Console.WriteLine("[server] Inside HttpGetCallback"); MyState myState = (MyState)asyncResult.AsyncState; HttpWebResponse resp; try { resp = (HttpWebResponse)myState.req.EndGetResponse(asyncResult); } catch (WebException e) { resp = (HttpWebResponse)e.Response; } myState.GETResponse = String.Format("Size of help page = {0}", resp.ContentLength); Console.WriteLine("[server] Inside HttpGetCallback, calling the user callback"); myState.UserCallback(new MyAsyncResult<MyState>(asyncResult, myState)); } public string EndProcess(IAsyncResult asyncResult) { Console.WriteLine("[server] Inside EndProcess"); MyAsyncResult<MyState> myAsyncResult = (MyAsyncResult<MyState>)asyncResult; MyState myState = myAsyncResult.AdditionalData; return String.Format("Original input: {0}; HTTP GET output: {1}", myState.input, myState.GETResponse); } } internal class MyUserStateBase { private AsyncCallback userCallback; private object userState; public MyUserStateBase(AsyncCallback userCallback, object userState) { this.userCallback = userCallback; this.userState = userState; } public AsyncCallback UserCallback { get { return this.userCallback; } } public object UserState { get { return this.userState; } } } internal class MyAsyncResult<T> : IAsyncResult where T : MyUserStateBase { private T additionalData; private IAsyncResult inner; public MyAsyncResult(IAsyncResult inner, T additionalData) { this.inner = inner; this.additionalData = additionalData; } public IAsyncResult Inner { get { return this.inner; } } public T AdditionalData { get { return this.additionalData; } } #region IAsyncResult Members public object AsyncState { get { return this.additionalData.UserState; } } public WaitHandle AsyncWaitHandle { get { return inner.AsyncWaitHandle; } } public bool CompletedSynchronously { get { return inner.CompletedSynchronously; } } public bool IsCompleted { get { return inner.IsCompleted; } } #endregion } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), ""); host.Open(); Console.WriteLine("Host opened");
ChannelFactory<ISyncTest> factory = new ChannelFactory<ISyncTest>(new BasicHttpBinding(), new EndpointAddress(baseAddress)); ISyncTest proxy = factory.CreateChannel(); Console.WriteLine(proxy.Process("Hello"));
((IClientChannel)proxy).Close(); factory.Close(); Console.Write("Press ENTER to close the host"); Console.ReadLine(); host.Close(); } }
|
-
The previous post mentioned how to return arbitrary data from WCF services. To receive data, however, there is one extra step, which I'll try to explain here.
Like returning arbitrary data, the key for accepting arbitrary (in any format) data is for a method to have a parameter with type System.IO.Stream. This parameter needs to be the single parameter which is passed in the body of the request. By that we mean that the operation can have other parameters beside the Stream one, as long as they're used in the address (UriTemplate) for the operation. For example, this program below will simulate an UploadFile operation:
public class BlogPostRaw2 { [ServiceContract] public interface ITest { [OperationContract, WebInvoke(UriTemplate = "UploadFile/{fileName}")] void UploadFile(string fileName, Stream fileContents); } public class Service : ITest { public void UploadFile(string fileName, Stream fileContents) { byte[] buffer = new byte[10000]; int bytesRead, totalBytesRead = 0; do { bytesRead = fileContents.Read(buffer, 0, buffer.Length); totalBytesRead += bytesRead; } while (bytesRead > 0); Console.WriteLine("Uploaded file {0} with {1} bytes", fileName, totalBytesRead); } } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "").Behaviors.Add(new WebHttpBehavior()); host.Open(); Console.WriteLine("Host opened");
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress + "/UploadFile/Test.txt"); req.Method = "POST"; req.ContentType = "text/plain"; Stream reqStream = req.GetRequestStream(); byte[] fileToSend = new byte[12345]; for (int i = 0; i < fileToSend.Length; i++) { fileToSend[i] = (byte)('a' + (i % 26)); } reqStream.Write(fileToSend, 0, fileToSend.Length); reqStream.Close(); HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription); host.Close(); } }
Notice that the (POST) HTTP request was sent to http://machine_name:8000/Service/UploadFile/<name_of_the_file_to_be_uploaded>; on the body of the request were the file contents.
One important note about the line in bold about Content-Type: when returning arbitrary data, specifying the content type is advisable, but not (necessarily) required. When sending arbitrary data to WCF, it is required. That is because the WebMessageEncoder (the inner piece of WCF, which is created when the endpoint uses the WebHttpBinding), needs it to be able to decode the message.
So this appears to works fine, until I decided to send a XML file to the server (showing only part of the client code):
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress + "/UploadFile/Test.xml"); req.Method = "POST"; req.ContentType = "text/xml"; Stream reqStream = req.GetRequestStream(); string fileContents = "<hello>world</hello>"; byte[] fileToSend = Encoding.UTF8.GetBytes(fileContents); reqStream.Write(fileToSend, 0, fileToSend.Length); reqStream.Close(); HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
When this request is sent to the server, the client gets a "400 Bad Request" response. A look at the server traces (enabled via instructions at http://msdn2.microsoft.com/en-us/library/ms733025.aspx) shows the following error:
System.InvalidOperationException: Incoming message for operation 'UploadFile' (contract 'ITest' with namespace 'http://tempuri.org/') contains an unrecognized http body format value 'Xml'. The expected body format value is 'Raw'. This can be because a WebContentTypeMapper has not been configured on the binding. See the documentation of WebContentTypeMapper for more details.
This is right on target. Basically, as I mentioned before, the WebMessageEncoder is actually composed of three "inner" encoders: XML, JSON and Raw. For content-types which map to the first two, the requests will be processed by then; only if neither the XML or the JSON encoder can process the content-type, the Raw will be used. For the first example it worked fine, since text/plain content cannot be processed by XML or JSON. So we need a way to "force" the encoder to always use Raw. As mentioned in the exception, the WebContentTypeMapper is the solution. The code below can now handle all content-types on request (modified parts in bold). Notice that we need to use a custom binding, since the mapper is only accessible via the WebMessageEncodingBindingElment directly, not in the standard WebHttpBinding.
public class BlogPostRaw2 { [ServiceContract] public interface ITest { [OperationContract, WebInvoke(UriTemplate = "UploadFile/{fileName}")] void UploadFile(string fileName, Stream fileContents); } public class Service : ITest { public void UploadFile(string fileName, Stream fileContents) { byte[] buffer = new byte[10000]; int bytesRead, totalBytesRead = 0; do { bytesRead = fileContents.Read(buffer, 0, buffer.Length); totalBytesRead += bytesRead; } while (bytesRead > 0); Console.WriteLine("Uploaded file {0} with {1} bytes", fileName, totalBytesRead); } } public class MyMapper : WebContentTypeMapper { public override WebContentFormat GetMessageFormatForContentType(string contentType) { return WebContentFormat.Raw; // always } } static Binding GetBinding() { CustomBinding result = new CustomBinding(new WebHttpBinding()); WebMessageEncodingBindingElement webMEBE = result.Elements.Find<WebMessageEncodingBindingElement>(); webMEBE.ContentTypeMapper = new MyMapper(); return result; } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), GetBinding(), "").Behaviors.Add(new WebHttpBehavior()); host.Open(); Console.WriteLine("Host opened");
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress + "/UploadFile/Test.xml"); req.Method = "POST"; req.ContentType = "text/xml"; Stream reqStream = req.GetRequestStream(); string fileContents = "<hello>world</hello>"; byte[] fileToSend = Encoding.UTF8.GetBytes(fileContents); reqStream.Write(fileToSend, 0, fileToSend.Length); reqStream.Close(); HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription); host.Close(); } }
|
-
I've seen quite a few times in the forums people asking how to control exactly how the data returned by a WCF service. People want to use a certain format for the output of the data which isn't (natively) supported by WCF, such as XML or JSON. A few examples of questions of this nature:
- I want to return the string "Hello world", but even though I set the BodyStyle property in the Web[Get/Invoke] attribute to Bare, the result is still wrapped in a <string> tag, like "<string xmlns="...">Hello world</string>; how do I remove the <string> wrapper?
- I need to return my parameters in a certain format which is not supported by WCF. For example, my operation returns a list of triples, and I want them to be returned in a .csv-like format, with the header names in the first line and the values in subsequent lines. How can I get it done?
One way to do it is to create a separate MessageEncoder, which is capable of taking a Message object and converting it to the bytes, and use that encoder in the binding of the endpoint which contains the operation. Although this is certainly doable, it requires a lot of work - a Message object contains an XML representation of its contents, so you'd need a mapping between XML and whichever format you want, possibly requiring a new XmlWriter/XmlReader implementation; you'll also need all the plumbing parts needed to connect the encoder with the endpoint (a MessageEncoderFactory, a MessageEncodingBindingElement and so on).
The Web programming model introduced in WCF on .NET Framework 3.5 simplifies this task. The magic happens when the operation return type is of System.IO.Stream (the abstract class, not one of its concrete implementations). By returning a stream, WCF assumes that the operation wants total control over the bytes that will be returned in the response, and will apply no formatting whatsoever in the data that is returned. This service, for example, solves the first question listed above:
[ ServiceContract] public class RawService { [OperationContract, WebGet] public System.IO.Stream GetValue() { string result = "Hello world"; byte[] resultBytes = Encoding.UTF8.GetBytes(result); return new MemoryStream(resultBytes); } }
This should work in most of the cases, since the result is fairly simple. It is a good practice, however, whenever you're using the raw programming model, to specify the Content-Type for the response. If nothing is specified, responses of operations with Stream return values will have a content type of application/octet-stream (i.e., binary). Some browsers (such as IE) may identify that the content is actually text, and print it correctly, but that's not a guarantee. The example will then become:
[ServiceContract] public class RawService { [OperationContract, WebGet] public System.IO.Stream GetValue() { string result = "Hello world"; byte[] resultBytes = Encoding.UTF8.GetBytes(result); WebOperationContext.Current.OutgoingResponse.ContentType = "text/plain"; return new MemoryStream(resultBytes); } }
Notice that this isn't limited to text only; with the raw programming model you can create a service which returns pretty much anything, in any format, such as images created on the fly:
public class BlogPostRaw { [ServiceContract] public interface ITest { [OperationContract, WebGet] Stream GetImage(int width, int height); } public class Service : ITest { public Stream GetImage(int width, int height) { Bitmap bitmap = new Bitmap(width, height); for (int i = 0; i < bitmap.Width; i++) { for (int j = 0; j < bitmap.Height; j++) { bitmap.SetPixel(i, j, (Math.Abs(i - j) < 2) ? Color.Blue : Color.Yellow); } } MemoryStream ms = new MemoryStream(); bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); ms.Position = 0; WebOperationContext.Current.OutgoingResponse.ContentType = "image/jpeg"; return ms; } } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "").Behaviors.Add(new WebHttpBehavior()); host.Open(); Console.WriteLine("Host opened"); Console.Write("Press ENTER to close the host"); Console.ReadLine(); host.Close(); } }
When running the test method, you can point the browser to http://localhost:8000/Service/GetImage?width=50&height=40 to see that WCF can also be used as an image server :)
|
-
In order for a Silverlight (or Flash) app coming from one domain to be able to consume data from services in a different domain, the service must "allow" the app to do so by providing a policy file which grants access (to prevent all sorts of cross-site scripting attacks). This policy file must be located in the root of the "domain" (hostname + port), so if your service is located at http://my.service.com:8000/Service/CoolService.svc/Endpoint, the policy file must be located at http://my.service.com:8000/ClientAccessPolicy.xml (or http://my.service.com:8000/crossdomain.xml in case of the Flash format). That's fairly easy to do on a IIS-hosted service (simply put the static policy file in the root of the web), but for self-hosted apps it isn't as simple (there's no "root" of the web).
To solve this problem for self-hosted WCF services, you can use the web programming model support fairly easily. Basically, you'd define the base address at the root of the domain, and have a web endpoint at the "" address. All the "real" service endpoints would then be in different addresses. The example below shows it in action:
public class SelfHostedServiceWithSilverlightPolicy { [ServiceContract] public interface ITest { [OperationContract] string Echo(string text); } [ServiceContract] public interface IPolicyRetriever { [OperationContract, WebGet(UriTemplate = "/clientaccesspolicy.xml")] Stream GetSilverlightPolicy(); [OperationContract, WebGet(UriTemplate = "/crossdomain.xml")] Stream GetFlashPolicy(); } public class Service : ITest, IPolicyRetriever { public string Echo(string text) { return text; } Stream StringToStream(string result) { WebOperationContext.Current.OutgoingResponse.ContentType = "application/xml"; return new MemoryStream(Encoding.UTF8.GetBytes(result)); } public Stream GetSilverlightPolicy() { string result = @"<?xml version=""1.0"" encoding=""utf-8""?> <access-policy> <cross-domain-access> <policy> <allow-from http-request-headers=""*""> <domain uri=""*""/> </allow-from> <grant-to> <resource path=""/"" include-subpaths=""true""/> </grant-to> </policy> </cross-domain-access> </access-policy>"; return StringToStream(result); } public Stream GetFlashPolicy() { string result = @"<?xml version=""1.0""?> <!DOCTYPE cross-domain-policy SYSTEM ""http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd""> <cross-domain-policy> <allow-access-from domain=""*"" /> </cross-domain-policy>"; return StringToStream(result); } } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "basic"); host.AddServiceEndpoint(typeof(IPolicyRetriever), new WebHttpBinding(), "").Behaviors.Add(new WebHttpBehavior()); ServiceMetadataBehavior smb = new ServiceMetadataBehavior(); smb.HttpGetEnabled = true; host.Description.Behaviors.Add(smb); host.Open(); Console.WriteLine("Host opened"); Console.Write("Press ENTER to close"); Console.ReadLine(); host.Close(); } }
|
-
Quite often one needs to talk to a WCF service, but using a (WCF) proxy is not a viable alternative. Sometimes the language used isn't a .NET one, the client might not have the .NET framework installed, or the overhead of the proxy is too big for the application need. In this case, creating a request "from scratch" (i.e., using sockets or HTTP requests directly) is usually the best option. In this case, the developer needs to find out the format of the message that is to be sent to the service.
The "correct" way of finding out this format is to look at the metadata from the service, and create a request that complies with all the assertions on the WSDL (or MEX policies). This is usually overkill for simple applications, as a simple template-based input would suffice. This post will present some ways of looking at the message sent from a WCF client (even though you'd not use one in production) and finding out this template.
1. Network capture (the easiest way)
If you can look at what is going on over the wire, you'll be able to see the message. I've found that Fiddler (http://www.fiddler2.com) is one of the best "men-in-the-middle" tools for this kind of task. It installs itself as a proxy in the machine, and any request that goes to http://<machine_name>... will be intercepted by it (notice that for it to work you can't use localhost, unless you configure the proxy settings to not bypass it). It has some problems on Vista, so sometimes it may not be feasible to use it. Also, if the transport of the message is not HTTP, this will not work either.
If you can split the client and server in two machines, then Netmon (http://www.microsoft.com/downloads/details.aspx?familyid=18b1d59d-f4d8-4213-8d17-2f6dde7d7aac&displaylang=en) will work all the times. This is also the best solution if you use TCP to talk to the service.
Network capture is the best way to deal with non-XML messages, as the next option will only show the XML representation of the message, even if it's encoded in some other form (such as JSON, MTOM or the binary encoding).
2. Message Logging (the WCF-only way)
WCF provides a way to log the incoming/outgoing messages, and this will allow you to see what the server is receiving. Enabling message logging with the SvcConfigEditor (http://msdn2.microsoft.com/en-us/library/ms732009.aspx, installed with the SDK) tool is very simple (select "Diagnostics", then "Enable Message Logging"), although I also like to add the "logEntireMessage='true'" option when trying to inspect messages. This config file below enables logging of all messages that flow through the service. The best way to look at the messages is with the SvcTraceViewer tool (http://msdn2.microsoft.com/en-us/library/ms732023.aspx, also installed with the SDK). If the trace viewer isn't available, I find it a quick solution to open the file with notepad, put a "<root>" in the beginning of it, a "</root>" at the end of it, and then opened it on IE (the message log file is an XML file, but it doesn't contain a root element).
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.diagnostics> <sources> <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing"> <listeners> <add name="ServiceModelMessageLoggingListener"> <filter type="" /> </add> </listeners> </source> </sources> <sharedListeners> <add initializeData="messages.svclog" type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="ServiceModelMessageLoggingListener" traceOutputOptions="Timestamp"> <filter type="" /> </add> </sharedListeners> </system.diagnostics> <system.serviceModel> <diagnostics> <messageLogging logMessagesAtTransportLevel="true" logEntireMessage="true"/> </diagnostics> </system.serviceModel> </configuration>
3. Message interception (harder way)
This is definitely overkill for this problem (finding out the messages themselves), so I'll leave their details to a future post. There are two alternatives here. One "less hard": add an instance of an IDispatchMessageInspector (http://msdn2.microsoft.com/en-us/library/system.servicemodel.dispatcher.idispatchmessageinspector.aspx) to the list of the inspectors on the endpoint (you'll need an endpoint behavior to do that). The other (harder) is the one that involves more work, which involves creating a custom MessageEncoder, which can look at the incoming/outgoing message bytes as they are on the wire, and then add all the plumbing required to add it to the binding (a MessageEncoderFactory and a MessageEncodingBindingElement). This custom message encoder would simply delegate the work to the "actual" encoder that is being used by the service.
Examples
Below is a service contract and some examples of requests to each of its three operations. The parts in bold in the requests are to be replaced by the actual values in the request.
[ DataContract] public class MyDC { [DataMember] public string str = "The string"; } [ServiceContract] public interface ITest { [OperationContract] string EchoString(string text); [OperationContract] int Add(int x, int y); [OperationContract] MyDC EchoDC(MyDC input); } public class Service : ITest { public string EchoString(string text) { return text; } public int Add(int x, int y) { return x + y; } public MyDC EchoDC(MyDC input) { return input; } } static Binding GetBinding() { BasicHttpBinding result = new BasicHttpBinding(); return result; } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), GetBinding(), ""); host.Open(); Console.WriteLine("Host opened");
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(GetBinding(), new EndpointAddress(baseAddress)); ITest proxy = factory.CreateChannel(); Console.WriteLine(proxy.EchoString("Hello")); Console.WriteLine(proxy.EchoDC(new MyDC())); Console.WriteLine(proxy.Add(3, 5));
((IClientChannel)proxy).Close(); factory.Close(); Console.Write("Press ENTER to close the host"); Console.ReadLine(); host.Close(); }
EchoDC:
POST /Service HTTP/1.1 Content-Type: text/xml; charset=utf-8 SOAPAction: "http://tempuri.org/ITest/EchoDC" Host: THE_MACHINE_NAME:8000 Content-Length: THE_ACTUAL_LENGTH
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><EchoDC xmlns="http://tempuri.org/"><input xmlns:a="http://schemas.datacontract.org/2004/07/WCFForums" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><a:str>The string</a:str></input></EchoDC></s:Body></s:Envelope>
Add:
POST /Service HTTP/1.1 Content-Type: text/xml; charset=utf-8 SOAPAction: "http://tempuri.org/ITest/Add" Host: THE_MACHINE_NAME:8000 Content-Length: THE_ACTUAL_LENGTH
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><Add xmlns="http://tempuri.org/"><x>3</x><y>5</y></Add></s:Body></s:Envelope>
EchoString:
POST /Service HTTP/1.1 Content-Type: text/xml; charset=utf-8 SOAPAction: "http://tempuri.org/ITest/EchoString" Host: THE_MACHINE_NAME:8000 Content-Length: THE_ACTUAL_LENGTH
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><EchoString xmlns="http://tempuri.org/"><text>Hello</text></EchoString></s:Body></s:Envelope>
|
-
When looking at a problem posted at the WCF forums (http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=118&SiteID=1), I usually try to set up a local reproduction of the issue to understand what is going on. One thing that helps me the most is to have a few templates that I can use to get a head start in the investigation. This very small piece of code can come quite handy for most of the issues I've seen so far:
//Template for WCF services public class PostXXXXXXX { [ServiceContract] public interface ITest { [OperationContract] string Echo(string text); } public class Service : ITest { public string Echo(string text) { return text; } } static Binding GetBinding() { BasicHttpBinding result = new BasicHttpBinding(); //Change binding settings here return result; } public static void Test() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), GetBinding(), ""); host.Open(); Console.WriteLine("Host opened");
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(GetBinding(), new EndpointAddress(baseAddress)); ITest proxy = factory.CreateChannel(); Console.WriteLine(proxy.Echo("Hello"));
((IClientChannel)proxy).Close(); factory.Close();
Console.Write("Press ENTER to close the host"); Console.ReadLine(); host.Close(); } }
Hope this helps.
|
-
There are some service settings which cannot be defined via config, only via code. If the service is hosted in a stand-alone application (via the ServiceHost class), this is not a problem - the host object is available, and from there one could use the object model to tweak all the service settings. When the service is hosted in IIS, however, this is not as simple - the main usage pattern to simply define the service and let IIS do all the hosting doesn't provide a hook for the ServiceHost object before the service is opened (and after that, most of the settings cannot be changed):
<% @ServiceHost Service="MyNamespace.MyService" %>
This directive will host the service MyNamespace.MyService in the default ServiceHost instance and open it when the first client connects to it. To perform some initialization on the host before it is opened, we need to use the Factory option of the directive to add some code before the host is actually opened:
<% @ServiceHost Service="MyNamespace.MyService" Factory="MyServiceHostFactory" Language="C#" debug="true" %> public class MyServiceHostFactory : ServiceHostFactory { protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { return new MyServiceHost(typeof(TestService), baseAddresses); } } public class MyServiceHost : ServiceHost { public MyServiceHost(Type serviceType, params Uri[] baseAddresses) : base(serviceType, baseAddresses) { } protected override void ApplyConfiguration() { base.ApplyConfiguration(); // Do any changes here. For example, this will set the contentTypeMapper option in one of the endpoints foreach (ServiceEndpoint endpoint in this.Description.Endpoints) { if (endpoint.Name == "TheEndpointThatNeedsToBeChanged") { CustomBinding newBinding = new CustomBinding(endpoint.Binding); WebMessageEncodingBindingElement webMEBE = newBinding.Elements.Find<WebMessageEncodingBindingElement>(); webMEBE.ContentTypeMapper = new MyWebContentTypeMapper(); endpoint.Binding = newBinding; } } } }
This was one of the solutions for the problem described in the (very long) thread at http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2348599&SiteID=1.
|
|
|
|