5

We have a web service up and running built in VS2010.

Several of the operational contracts looks like this:

    [OperationContract]
    ITicket Login(string userName, byte[] passwordHash, string softwareVersion);

I.e. they booth have complex arguments and complex return types, or even multiple returns.

We have recently started an outsourced iPhone project and are letting them use this service to communicate with our server. From what I have learnt from them I understood that this is not a good practice for communicating to the iPhone (lack of good ways to consume the WSDL for example). And therefore I have started to look at the possibility to expose the service as a REST service communicating with JSON.

I have added a new endpoint, using webHttpBinding, decorated the contracts like this:

    [OperationContract]
    [WebGet(UriTemplate = "/login?username={userName}&password={password}&softwareVersion={softwareVersion}", ResponseFormat=WebMessageFormat.Json)]
    ITicket Login(string userName, string password, string softwareVersion);

This method now works as intended.

I then tried to decorate another method as this:

    [OperationContract]
    [WebGet(UriTemplate = "/GetMetaData?ticket={ticket}",RequestFormat=WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    IMetaData GetMetaData(ITicket ticket);

When I now try to access this I receive the following error:

Server Error in '/Jetas5MobileService' Application. Operation 'GetMetaData' in contract 'IJetas5MobileService2' has a query variable named 'ticket' of type 'Jetas.MobileService.DataContracts.ITicket', but type 'Jetas.MobileService.DataContracts.ITicket' is not convertible by 'QueryStringConverter'. Variables for UriTemplate query values must have types that can be converted by 'QueryStringConverter'.

I have manage to build a OperationContract that only takes a string as argument and then parses thin in the back end by using DataContractJsonSerializer, but that feels more like a ugly hack.

Is there any way to solve this in a better manner? I am beginner when it comes to WCF and REST so do not be afraid to point me to any beginner tutorials that there might be out there. I have tried to search for them but the vast amount of sources makes it hard to find the good ones.

nj.
  • 756
  • 1
  • 5
  • 21

3 Answers3

2

I faced the similar problem using WCF Rest Starter Kit.

If I remember correctly, UriTemplate variables in the path always resolve to strings when using WebGet or WebInvoke. You can only bind UriTemplate variables to int, long, etc. when they are in the query portion of the UriTemplate. So there is no means to pass a complex object in.

I think there is no clean way to do it. I just used the parsing solution as you do.

Now, you can check out the new stack for doing REST with WCF called WCF Web Api. It deals very nicely with complex types as method parameters.

Tomasz Jaskuλa
  • 15,723
  • 5
  • 46
  • 73
2

From what I have learnt from them I understood that this is not a good practice for communicating to the iPhone (lack of good ways to consume the WSDL for example).

The biggest problem is not the lack of good "tools" but the lack of understanding what WSDL is and how web services work. All these tools generating service stubs for developers caused that developers don't understand what is under the hood. It works for basic scenarios where all magic is done for you but once developers have to track any problem or extend the "tool" with additional features they have big problems (and it usually result in bad solution). To be honest SW development is not about basic scenarios.

REST place a big challenge for developers because it doesn't provide any "magic" tools. REST is about correct usage of HTTP protocol and it takes full advantage of existing HTTP infrastracture. Without understanding basics of HTTP protocol you will not create good REST service. Thats where you should begin.

Here is some example of incorrect usage:

[OperationContract]
[WebGet(UriTemplate = "/login?username={userName}&password={password}&softwareVersion={softwareVersion}", ResponseFormat=WebMessageFormat.Json)]
ITicket Login(string userName, string password, string softwareVersion);

Login method is obviously something that perform some action - I guess it creates ticket. It is absolutely unsuitable for GET HTTP request. This should definitely be a POST request to Login resource returning new ITicket representation for each call. Why? Because GET requests are supposed to be safe and idempotent.

  • Safe: the request should not cause any side effects = it should not do any changes to the resource but in your case it most probably creates a new resource.
  • Idempotent: this one is not so important for the example because you have already violated the Safe rule but it means that request to the resource should be repeatable. It means that first request with same user name, password and version can create new resource but when the request is executed again it should not create a new resource but return already created one. This makes more sense when the resource is persisted / maintained on the server.

Because HTTP GET request is by HTTP infrastructure considered as safe and idempotent it is handled in different way. For example GET requests can be cached redirected etc. When the request is not safe and idempotent it should use POST method. So the correct definition is:

[OperationContract]
[WebInvoke(UriTemplate = "/login?username={userName}&password={password}&softwareVersion={softwareVersion}", ResponseFormat=WebMessageFormat.Json)]
ITicket Login(string userName, string password, string softwareVersion);

because WebInvoke defaults to POST method. That is also reason why all protocol tunneling (for example SOAP) use usually POST HTTP methods for all requests.

The other problem in former example can again be REST approach = taking full advantage of HTTP infrastrucuture. It should use HTTP based authentication (login) = Basic, Digest, OAuth, etc. It doesn't mean that you cannot have similar resource but you should first consider using standard HTTP way.

Your second example is actually much better but it has problem with WCF limitation. WCF can read from URL only basic types (btw. how do you want to pass object in URL?). Any other parameter type needs custom WCF behavior. If you need to expose method which accepts data contract you must again use the HTTP method which accepts parameters in body - again use POST and place JSON serialized ticket to the body of the request:

[OperationContract]
[WebInvoke(UriTemplate = "/GetMetaData",RequestFormat=WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
IMetaData GetMetaData(ITicket ticket);
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • Thanks for the great feedback! I will read up more on the HTTP protocol. I have one question already though. The GetMetaData is basically a query that returns data, depending on the ticket. When I read http://www.w3.org/2001/tag/doc/whenToUseGet.html#checklist I interpret this as GET would be the most suitable for this action, i.e. the state of the server is the same. Is it because of the complex type of the argument that POST is preferred? – nj. Nov 18 '11 at 06:20
  • Yes it is more suitable for GET but because of WCF default feature set your method definition requires POST to accept complex type. – Ladislav Mrnka Nov 18 '11 at 09:48
1

You should post JSON data to the method and can setup the declaration like:

[OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json,
        UriTemplate = "/login", BodyStyle = WebMessageBodyStyle.Wrapped)]
    ITicket Login(string userName, string password, string softwareVersion);

Then add a new endpoint to your configuration as follows (leaving your existing endpoints and configuration the way it stands, you are just adding the new JSON endpoint and a new behavior):

    <service behaviorConfiguration="Your.ServiceBehavior.Here" name="Your.Stuff.Here">
        <endpoint address="" binding="basicHttpBinding" bindingConfiguration="basicBindingSettings" behaviorConfiguration="basic" contract="Your.Contract.Here">
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
        <endpoint address="json" binding="webHttpBinding" contract="Your.Contract.Here" behaviorConfiguration="web"></endpoint>
      </service>
<behaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp />
        </behavior>
    </behaviors>

Then you can post something like "{ "userName" : "testuser", "password" : "testpass", "softwareVersion" : "1.0.0" }" to the URL https://yourdomain.com/service.svc/json/login.

If you want to pass in a complex type then you just have to pass in JSON that matches the custom object. So if you had an animal object with color and size properties the JSON would look like "{ "animal" : { "Color" : "red", "Size" : "Large" } }".

That should about do it and you don't need to alter the implementation of the method at all. WCF will just return JSON formatted data when invoked in the manner above, against the JSON endpoint. Your existing SOAP methods will continue to work as normal.

GCaiazzo
  • 131
  • 3
  • Both my endpoint and the method "login" works for me. It is when I try with the more complex types the error starts. – nj. Nov 18 '11 at 06:06
  • I realize you had some things included in my post already accomplished, but I was going through the steps from top to bottom for completeness, just in case someone else stumbles upon this. The key with complex types in the scenario I described is setting the RequestFormat = WebMessageFormat.Json and then properly formatting your JSON request. – GCaiazzo Nov 20 '11 at 06:06