Search code examples
wcfresturlencodeurl-encoding

WCF REST read URL-encoded response


I'm trying to use WCF to consume a 3rd-party REST service which responds with URL-encoded data:

a=1&b=2&c=3

I have this now:

[DataContract]
class Response { 
  [DataMember(Name="a")]
  public int A { get;set;}
  [DataMember(Name="b")]
  public int B { get;set;}
  [DataMember(Name="c")]
  public int C { get;set;}
}

[ServiceContract]
interface IService
{
  [OperationContract]
  Response Foo();
}

But it comes back with:

There was an error checking start element of object of type Response. The data at the root level is invalid. Line 1, position 1.

Solution

  • WCF does not "understand" forms-data content type (application/x-www-forms-urlencoded), so it won't be able to read that response directly. You can either implement a message formatter which will be able to convert that format into your contract, or you can receive the response as a Stream (which will give you the full bytes of the response), which you can decode into your class.

    The code below shows how you could implement a formatter for this operation. It's not generic, but you should get the picture of what needs to be done.

    public class StackOverflow_16493746
    {
        [ServiceContract]
        public class Service
        {
            [WebGet(UriTemplate = "*")]
            public Stream GetData()
            {
                WebOperationContext.Current.OutgoingResponse.ContentType = "application/x-www-form-urlencoded";
                return new MemoryStream(Encoding.UTF8.GetBytes("a=1&b=2&c=3"));
            }
        }
        [ServiceContract]
        interface IService
        {
            [WebGet]
            Response Foo();
        }
        [DataContract]
        class Response
        {
            [DataMember(Name = "a")]
            public int A { get; set; }
            [DataMember(Name = "b")]
            public int B { get; set; }
            [DataMember(Name = "c")]
            public int C { get; set; }
        }
        public class MyResponseFormatter : IClientMessageFormatter
        {
            private IClientMessageFormatter originalFormatter;
    
            public MyResponseFormatter(IClientMessageFormatter originalFormatter)
            {
                this.originalFormatter = originalFormatter;
            }
    
            public object DeserializeReply(Message message, object[] parameters)
            {
                HttpResponseMessageProperty httpResp = (HttpResponseMessageProperty)
                    message.Properties[HttpResponseMessageProperty.Name];
                if (httpResp.Headers[HttpResponseHeader.ContentType] == "application/x-www-form-urlencoded")
                {
                    if (parameters.Length > 0)
                    {
                        throw new InvalidOperationException("out/ref parameters not supported in this formatter");
                    }
    
                    byte[] bodyBytes = message.GetReaderAtBodyContents().ReadElementContentAsBase64();
                    NameValueCollection pairs = HttpUtility.ParseQueryString(Encoding.UTF8.GetString(bodyBytes));
                    Response result = new Response();
                    foreach (var key in pairs.AllKeys)
                    {
                        string value = pairs[key];
                        switch (key)
                        {
                            case "a":
                                result.A = int.Parse(value);
                                break;
                            case "b":
                                result.B = int.Parse(value);
                                break;
                            case "c":
                                result.C = int.Parse(value);
                                break;
                        }
                    }
    
                    return result;
                }
                else
                {
                    return this.originalFormatter.DeserializeReply(message, parameters);
                }
            }
    
            public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
            {
                throw new NotSupportedException("This is a reply-only formatter");
            }
        }
        public class MyClientBehavior : WebHttpBehavior
        {
            protected override IClientMessageFormatter GetReplyClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
            {
                return new MyResponseFormatter(base.GetReplyClientFormatter(operationDescription, endpoint));
            }
        }
        public static void Test()
        {
            string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
            WebServiceHost host = new WebServiceHost(typeof(Service), new Uri(baseAddress));
            host.Open();
            Console.WriteLine("Host opened");
    
            ChannelFactory<IService> factory = new ChannelFactory<IService>(new WebHttpBinding(), new EndpointAddress(baseAddress));
            factory.Endpoint.Behaviors.Add(new MyClientBehavior());
            IService proxy = factory.CreateChannel();
            Response resp = proxy.Foo();
            Console.WriteLine("a={0},b={1},c={2}", resp.A, resp.B, resp.C);
    
            ((IClientChannel)proxy).Close();
            factory.Close();
    
            Console.Write("Press ENTER to close the host");
            Console.ReadLine();
            host.Close();
        }
    }