0

I want to add more parameters to my JSON WebService without breaking call from old clients. Example:

My WebService in Service.asmx

[WebMethod]
public string Ping(string msg, string additionalInfo)
{
    if (string.IsNullOrEmpty(additionalInfo))
    {
        return "Process msg with old version";
    }
    return "Process msg with new version"; ;
}

//My old web service does not have additionalInfo arguments
//public string Ping(string msg) {..}

Web.config tells that my WebService is JSON-based

<system.web.extensions>
  <scripting>
       <webServices>
           <jsonSerialization maxJsonLength="50000000" />
       </webServices>
   </scripting>

if clients call my new Json WebService with all the parameters => everything is fine

CallWs("http://localhost:48918/Service.asmx/Ping", '{"msg":"hello", "additionalInfo":""}')

But all the current clients won't give the additionalInfo:

CallWs("http://localhost:48918/Service.asmx/Ping", '{"msg":"hello"}')

my new WebService will immediately return error:

string(654) "{"Message":"Invalid web service call, missing value for parameter: \u0027additionalInfo\u0027.","StackTrace":"   at System.Web.Script.Services.WebServiceMethodData.CallMethod(Object target, IDictionary`2 parameters)\r\n   at System.Web.Script.Services.WebServiceMethodData.CallMethodFromRawParams(Object target, IDictionary`2 parameters)\r\n   at System.Web.Script.Services.RestHandler.InvokeMethod(HttpContext context, WebServiceMethodData methodData, IDictionary`2 rawParams)\r\n   at System.Web.Script.Services.RestHandler.ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData)","ExceptionType":"System.InvalidOperationException"}"

So my customers will have to change theire code in order to use my new WebService, I don't want that. I want to give default values to my new WebService, What is the best way to do?

Possible duplication: Can I have an optional parameter for an ASP.NET SOAP web service But none of the response works for me.


FYI my customers often call my JSON WebService via PHP, they simply make a POST request to the service endpoint:

$ch = curl_init("http://localhost:48918/Service.asmx/Ping");
$wsrq = array(
    "msg" => "Hello",
    //"additionalInfo" => "World",
);
curl_setopt_array($ch, array(
    CURLOPT_POST => TRUE,
    CURLOPT_RETURNTRANSFER => TRUE,
    CURLOPT_SSL_VERIFYPEER => FALSE,
    CURLOPT_POSTFIELDS => json_encode($wsrq),
    CURLOPT_HTTPHEADER => array("Content-type: application/json; charset=utf-8"),
));
$response = curl_exec($ch);
Community
  • 1
  • 1
Hiep
  • 2,483
  • 1
  • 25
  • 32
  • try make it optional parameters ? public string Ping(string msg, string additionalInfo=null) – Frebin Francis Dec 31 '15 at 09:50
  • or write an overloaded function extra in the web service. – Frebin Francis Dec 31 '15 at 09:52
  • doesn't work! it was the first thing I tried.. – Hiep Dec 31 '15 at 09:52
  • you tried with an overloaded function ? – Frebin Francis Dec 31 '15 at 09:53
  • i think you can achieve optional parameters by overloads using the MessageName property. – Frebin Francis Dec 31 '15 at 09:54
  • How is this web service called? I am surprised that adding a parameter will break it, especially if it is a JSON enabled service. What's this `CallWs` method doing more specifically? – Darin Dimitrov Dec 31 '15 at 09:55
  • I don't like the overloaded function, overtime, I will have a lot of version for Ping serivces, I will have a lot of overloaded function Ping1(), Ping2(), Ping3().. which bloated my API, new customer will have to choose between them.. – Hiep Dec 31 '15 at 09:57
  • @Darin Dimitrov I added client code in the question. The client make a Post request to the webservice. And the WS doesn't like the missing argument requests.. – Hiep Dec 31 '15 at 10:11
  • Thanks, I am able to reproduce the issue. That got me surprised, honestly I wouldn't have expected this to break. Looks like the only way to achieve that is by using method overloads. – Darin Dimitrov Dec 31 '15 at 10:16

1 Answers1

1

It looks like the proper way to achieve that is to use method overloads for your service methods. Also for the future methods I would recommend you using models:

public class MyModel
{
    public string Message { get; set; }
    public string AdditionalInfo { get; set; }
}

and then:

[WebMethod]
public string Ping(MyModel model)
{
    ...
}

This will give you more flexibility because you will be able to add properties easily in the future without breaking.

This being said, there's one approach or a workaround that you might consider: manual deserialization (I totally don't recommend it but worth mentioning).

Make your WebMethod without any parameters:

[WebMethod]
public string Ping()

and then read the request body manually by accessing the input stream:

[WebMethod]
public string Ping()
{
    Context.Request.InputStream.Seek(0, SeekOrigin.Begin);
    using (var inputStream = Context.Request.InputStream)
    using (var reader = new StreamReader(inputStream))
    {
        string body = reader.ReadToEnd();

        // TODO: Worth checking the request headers before attempting JSON deserialization
        // For example the Content-Type header
        var model = JsonConvert.DeserializeObject<MyModel>(body);
        if (string.IsNullOrEmpty(model.AdditionalInfo))
        {
            return "Process msg with old version";
        }
        return "Process msg with new version"; ;
    }
}

To avoid mixing multiple responsibilities in your service method you could move the parsing of the body stream into some separate extension method:

public static class RequestExtensions
{
    public static T ParseRequest<T>(this HttpRequest request)
    {
        request.InputStream.Seek(0, SeekOrigin.Begin);
        using (var inputStream = request.InputStream)
        using (var reader = new StreamReader(inputStream))
        {
            string body = reader.ReadToEnd();
            return JsonConvert.DeserializeObject<T>(body);
        }
    }
}

and then your WebMethod:

[WebMethod]
public string Ping()
{
    var model = Context.Request.ParseRequest<MyModel>();
    if (string.IsNullOrEmpty(model.AdditionalInfo))
    {
        return "Process msg with old version";
    }
    return "Process msg with new version"; ;
}

Now clients can call the Ping method like that:

POST /WebService1.asmx/Ping HTTP/1.1
Content-type: application/json; charset=utf-8
Host: localhost:14529
Content-Length: 61

{
    "msg": "Hello",
    "additionalInfo": "add info"
}

or the old way:

POST /WebService1.asmx/Ping HTTP/1.1
Content-type: application/json; charset=utf-8
Host: localhost:14529
Content-Length: 26

{
    "msg": "Hello"
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • change from Ping(string) to Ping(MyModel) is also a breaking changes we want to avoid. But it seems there is no better way. thanks – Hiep Dec 31 '15 at 14:13
  • Yes, I know that it is breaking change. I mentioned it for your V2. So that you can easily do V3 at a later stage. In all cases you should definitely consider migrating away from this deprecated technology which is what classic ASP.NET webservices represent now. – Darin Dimitrov Dec 31 '15 at 14:14