locked
WCF REST WebGet - accepting arrays of parameters RRS feed

  • Question

  • Hi I have a REST service which has to get invoices.

    So I have this URI:

    /MyService/invoice/<id>

    So /MyService/invoice/25253 would get me invoice id 25253

    Now, I also need a mechanism for getting multiple invoices in one request, so I want the service to accept a list of ids.

    I have a couple of questions around this...

    - Can it be done via the URI, something like: /MyService/invoice/264,225,325,226,887,123

    - Or does it need to be on the queryString:  /MyService/invoice?ids=264,225,325,226,887,123

    - Or can it not really be done with a HTTP GET?  Do I need a POST which accepts an array as a parameter?

    I saw an article which used a custom query string converter to take a parameter like ids=264,225,325,226,887,123 and convert it to an int[] but I'm not sure what's the best way to go.

    Thanks for any help!

    Paul

    Friday, April 23, 2010 8:58 AM

Answers

  • It can be done both ways. The query string solution is the one that fits easier with the WCF model (see code below). To use the URI option, you'd need to receive the parameter as string (Uri "parts" need to be string parameters), and do the conversion in the method itself.

      public class Post_2341c11e_92b3_4da4_aba5_858054f46c80
      {
        [ServiceContract]
        public interface ITest
        {
          [OperationContract, WebGet]
          int Sum(int[] ids);
          [OperationContract, WebGet(UriTemplate = "/invoice/{ids}")]
          int Invoices(string ids);
        }
        public class Service : ITest
        {
          public int Sum(int[] ids)
          {
            if (ids == null) return 0;
            int result = 0;
            foreach (int id in ids) result += id;
            return result;
          }
          public int Invoices(string ids)
          {
            string[] parts = ids.Split(',');
            int result = 0;
            foreach (string part in parts) result += int.Parse(part);
            return result;
          }
        }
        public class MyBehavior : WebHttpBehavior
        {
          protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
          {
            return new MyConverter(base.GetQueryStringConverter(operationDescription));
          }
        }
        public class MyConverter : QueryStringConverter
        {
          QueryStringConverter inner;
          public MyConverter(QueryStringConverter inner)
          {
            this.inner = inner;
          }
          public override bool CanConvert(Type type)
          {
            return type == typeof(int[]) || inner.CanConvert(type);
          }
          public override object ConvertStringToValue(string parameter, Type parameterType)
          {
            if (parameterType == typeof(int[]))
            {
              string[] parts = parameter.Split(',');
              int[] result = new int[parts.Length];
              for (int i = 0; i < parts.Length; i++) result[i] = int.Parse(parts[i]);
              return result;
            }
    
            return inner.ConvertStringToValue(parameter, parameterType);
          }
          // The override below is not needed if used only at the server
          public override string ConvertValueToString(object parameter, Type parameterType)
          {
            if (parameterType == typeof(int[]))
            {
              int[] intArray = (int[])parameter;
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < intArray.Length; i++)
              {
                if (i > 0) sb.Append(',');
                sb.Append(intArray[i]);
              }
    
              return sb.ToString();
            }
    
            return inner.ConvertValueToString(parameter, parameterType);
          }
        }
        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 MyBehavior());
          host.Open();
          Console.WriteLine("Host opened");
    
          Util.SendRequest(baseAddress + "/Sum?ids=123,345,121", "GET", null, null);
          Util.SendRequest(baseAddress + "/invoice/123,345,121", "GET", null, null);
    
          Console.Write("Press ENTER to close the host");
          Console.ReadLine();
          host.Close();
        }
      }
      public static class Util
      {
        public static string SendRequest(string uri, string method, string contentType, string body)
        {
          string responseBody = null;
    
          HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
          req.Method = method;
          if (!String.IsNullOrEmpty(contentType))
          {
            req.ContentType = contentType;
          }
          if (!String.IsNullOrEmpty(body))
          {
            byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
            req.GetRequestStream().Write(bodyBytes, 0, bodyBytes.Length);
            req.GetRequestStream().Close();
          }
    
          HttpWebResponse resp;
          try
          {
            resp = (HttpWebResponse)req.GetResponse();
          }
          catch (WebException e)
          {
            resp = (HttpWebResponse)e.Response;
          }
          Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
          foreach (string headerName in resp.Headers.AllKeys)
          {
            Console.WriteLine("{0}: {1}", headerName, resp.Headers[headerName]);
          }
          Console.WriteLine();
          Stream respStream = resp.GetResponseStream();
          if (respStream != null)
          {
            responseBody = new StreamReader(respStream).ReadToEnd();
            Console.WriteLine(responseBody);
          }
          else
          {
            Console.WriteLine("HttpWebResponse.GetResponseStream returned null");
          }
          Console.WriteLine();
          Console.WriteLine(" *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* ");
          Console.WriteLine();
    
          return responseBody;
        }
      }
    
    • Marked as answer by Bin-ze Zhao Wednesday, April 28, 2010 10:59 AM
    Friday, April 23, 2010 4:53 PM
  • If you're hosting the service in IIS, then the limit of the URI is around 2k (http://support.microsoft.com/default.aspx/kb/208427?p=1). If you're hosting the service yourself (as in the code I sent), the limit is higher (around 16k), but you still cannot send very large request URIs.

    For very large parameters, you should consider using a POST request instead of GET, exactly because of this limitation.

    • Marked as answer by Bin-ze Zhao Wednesday, April 28, 2010 10:59 AM
    Monday, April 26, 2010 6:52 PM

All replies

  • It can be done both ways. The query string solution is the one that fits easier with the WCF model (see code below). To use the URI option, you'd need to receive the parameter as string (Uri "parts" need to be string parameters), and do the conversion in the method itself.

      public class Post_2341c11e_92b3_4da4_aba5_858054f46c80
      {
        [ServiceContract]
        public interface ITest
        {
          [OperationContract, WebGet]
          int Sum(int[] ids);
          [OperationContract, WebGet(UriTemplate = "/invoice/{ids}")]
          int Invoices(string ids);
        }
        public class Service : ITest
        {
          public int Sum(int[] ids)
          {
            if (ids == null) return 0;
            int result = 0;
            foreach (int id in ids) result += id;
            return result;
          }
          public int Invoices(string ids)
          {
            string[] parts = ids.Split(',');
            int result = 0;
            foreach (string part in parts) result += int.Parse(part);
            return result;
          }
        }
        public class MyBehavior : WebHttpBehavior
        {
          protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
          {
            return new MyConverter(base.GetQueryStringConverter(operationDescription));
          }
        }
        public class MyConverter : QueryStringConverter
        {
          QueryStringConverter inner;
          public MyConverter(QueryStringConverter inner)
          {
            this.inner = inner;
          }
          public override bool CanConvert(Type type)
          {
            return type == typeof(int[]) || inner.CanConvert(type);
          }
          public override object ConvertStringToValue(string parameter, Type parameterType)
          {
            if (parameterType == typeof(int[]))
            {
              string[] parts = parameter.Split(',');
              int[] result = new int[parts.Length];
              for (int i = 0; i < parts.Length; i++) result[i] = int.Parse(parts[i]);
              return result;
            }
    
            return inner.ConvertStringToValue(parameter, parameterType);
          }
          // The override below is not needed if used only at the server
          public override string ConvertValueToString(object parameter, Type parameterType)
          {
            if (parameterType == typeof(int[]))
            {
              int[] intArray = (int[])parameter;
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < intArray.Length; i++)
              {
                if (i > 0) sb.Append(',');
                sb.Append(intArray[i]);
              }
    
              return sb.ToString();
            }
    
            return inner.ConvertValueToString(parameter, parameterType);
          }
        }
        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 MyBehavior());
          host.Open();
          Console.WriteLine("Host opened");
    
          Util.SendRequest(baseAddress + "/Sum?ids=123,345,121", "GET", null, null);
          Util.SendRequest(baseAddress + "/invoice/123,345,121", "GET", null, null);
    
          Console.Write("Press ENTER to close the host");
          Console.ReadLine();
          host.Close();
        }
      }
      public static class Util
      {
        public static string SendRequest(string uri, string method, string contentType, string body)
        {
          string responseBody = null;
    
          HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
          req.Method = method;
          if (!String.IsNullOrEmpty(contentType))
          {
            req.ContentType = contentType;
          }
          if (!String.IsNullOrEmpty(body))
          {
            byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
            req.GetRequestStream().Write(bodyBytes, 0, bodyBytes.Length);
            req.GetRequestStream().Close();
          }
    
          HttpWebResponse resp;
          try
          {
            resp = (HttpWebResponse)req.GetResponse();
          }
          catch (WebException e)
          {
            resp = (HttpWebResponse)e.Response;
          }
          Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
          foreach (string headerName in resp.Headers.AllKeys)
          {
            Console.WriteLine("{0}: {1}", headerName, resp.Headers[headerName]);
          }
          Console.WriteLine();
          Stream respStream = resp.GetResponseStream();
          if (respStream != null)
          {
            responseBody = new StreamReader(respStream).ReadToEnd();
            Console.WriteLine(responseBody);
          }
          else
          {
            Console.WriteLine("HttpWebResponse.GetResponseStream returned null");
          }
          Console.WriteLine();
          Console.WriteLine(" *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* ");
          Console.WriteLine();
    
          return responseBody;
        }
      }
    
    • Marked as answer by Bin-ze Zhao Wednesday, April 28, 2010 10:59 AM
    Friday, April 23, 2010 4:53 PM
  • Hi Carlos,

    Thanks for the reply - very helpful indeed and great examples!

    One question on these solutions....is there a restriction on the URI/Query string length?  The reason I ask is that in some cases the list of ID's could be quite long (a few thousand!).  I implemented your example which worked great, but when I send in a very long request I get a HTTP 400 (Bad Request) from the server.

    Paul

    Monday, April 26, 2010 8:36 AM
  • If you're hosting the service in IIS, then the limit of the URI is around 2k (http://support.microsoft.com/default.aspx/kb/208427?p=1). If you're hosting the service yourself (as in the code I sent), the limit is higher (around 16k), but you still cannot send very large request URIs.

    For very large parameters, you should consider using a POST request instead of GET, exactly because of this limitation.

    • Marked as answer by Bin-ze Zhao Wednesday, April 28, 2010 10:59 AM
    Monday, April 26, 2010 6:52 PM
  • Hi

    NOTE TO ALL.

    None of this code works. There is a bug logged.

    http://connect.microsoft.com/VisualStudio/feedback/details/616486/bug-with-getquerystringconverter-not-being-called-by-webservicehost#tabs

    Please make sure you check the bug before wasting hours of your time trying to create custom query converters that cannot possibly work.


    Regards
    Craig.

    Friday, May 27, 2011 6:36 AM
  • The code listed there does work - just try it, it's F5-ready. The bug is about using the WebServiceHost with QueryStringConverter. In this example I use a "normal" service host. I'll post a new workaround in the connect site.

    Below is an updated version of the previous code, which also supports Nullable<T> parameters.

     

      public class Post_2341c11e_92b3_4da4_aba5_858054f46c80
      {
        [ServiceContract]
        public interface ITest
        {
          [OperationContract, WebGet]
          int Sum(int[] ids);
          [OperationContract, WebGet(UriTemplate = "/invoice/{ids}")]
          int Invoices(string ids);
          [OperationContract, WebGet]
          string GetValue(Nullable<int> ni, Nullable<double> nd);
        }
        public class Service : ITest
        {
          public int Sum(int[] ids)
          {
            if (ids == null) return 0;
            int result = 0;
            foreach (int id in ids) result += id;
            return result;
          }
          public int Invoices(string ids)
          {
            string[] parts = ids.Split(',');
            int result = 0;
            foreach (string part in parts) result += int.Parse(part);
            return result;
          }
          public string GetValue(Nullable<int> ni, Nullable<double> nd)
          {
            return string.Format("ni={0},nd={1}",
              ni == null ? "<null>" : ni.Value.ToString(),
              nd == null ? "<null>" : nd.Value.ToString());
          }
        }
        public class MyBehavior : WebHttpBehavior
        {
          protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
          {
            return new MyConverter(base.GetQueryStringConverter(operationDescription));
          }
        }
        public class MyConverter : QueryStringConverter
        {
          QueryStringConverter inner;
          public MyConverter(QueryStringConverter inner)
          {
            this.inner = inner;
          }
          public override bool CanConvert(Type type)
          {
            return type == typeof(int[]) || type == typeof(int?) || type == typeof(double?) || inner.CanConvert(type);
          }
          public override object ConvertStringToValue(string parameter, Type parameterType)
          {
            if (parameterType == typeof(int[]))
            {
              string[] parts = parameter.Split(',');
              int[] result = new int[parts.Length];
              for (int i = 0; i < parts.Length; i++) result[i] = int.Parse(parts[i]);
              return result;
            }
            else if (parameterType == typeof(int?))
            {
              int temp;
              if (int.TryParse(parameter, out temp))
              {
                return temp;
              }
              else
              {
                return null;
              }
            }
            else if (parameterType == typeof(double?))
            {
              double temp;
              if (double.TryParse(parameter, out temp))
              {
                return temp;
              }
              else
              {
                return null;
              }
            }
    
            return inner.ConvertStringToValue(parameter, parameterType);
          }
          // The override below is not needed if used only at the server
          public override string ConvertValueToString(object parameter, Type parameterType)
          {
            if (parameterType == typeof(int[]))
            {
              int[] intArray = (int[])parameter;
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < intArray.Length; i++)
              {
                if (i > 0) sb.Append(',');
                sb.Append(intArray[i]);
              }
    
              return sb.ToString();
            }
    
            return inner.ConvertValueToString(parameter, parameterType);
          }
        }
        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 MyBehavior());
          host.Open();
          Console.WriteLine("Host opened");
    
          Util.SendRequest(baseAddress + "/Sum?ids=123,345,121", "GET", null, null);
          Util.SendRequest(baseAddress + "/invoice/123,345,121", "GET", null, null);
          Util.SendRequest(baseAddress + "/GetValue?ni=null&nd=123.45", "GET", null, null);
          Util.SendRequest(baseAddress + "/GetValue?ni=222&nd=123.45", "GET", null, null);
    
          Console.Write("Press ENTER to close the host");
          Console.ReadLine();
          host.Close();
        }
      }
    
    Friday, May 27, 2011 5:45 PM
  • Hi Carlos,

    Thanks for clearing that up. I implemented your code into my exising service which uses WebServiceHost and hence the problem. I will try and setup the bindings manually and see if I can get it working with a regular service host.

    I need to figure out what WebServiceHost does with streaming, because I am using streams in my code, and apparently the WebServiceHost also adds some additional configuration for streaming.

    I will see if I can get it all working together now.
    It's still a bad bug.

    Regards
    Craig.

    Wednesday, June 1, 2011 6:57 AM