Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WCF Complex JSON INPUT Error (not convertible by QueryStringConverter)

Tags:

json

wcf

Im having issue geting Complex JSON working as a parameter in my WCF Service.

Using, Microsoft.Net 3.5 SP1 in Visual Studio 2008 SP1

With the following Contract:

[ServiceContract]
public interface IService1
{

    [OperationContract]
    [WebGet(UriTemplate="GetDataUsingDataContract?composite={composite}", 
        BodyStyle=WebMessageBodyStyle.Wrapped, 
        RequestFormat=WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    CompositeType GetDataUsingDataContract(CompositeType composite);

    // TODO: Add your service operations here
}

// Use a data contract as illustrated in the sample below to add composite types to service operations
[DataContract]
public class CompositeType
{
    string boolValue = "true";
    string stringValue = "Hello ";

    [DataMember]
    public string BoolValue
    {
        get { return boolValue; }
        set { boolValue = value; }
    }

    [DataMember]
    public string StringValue
    {
        get { return stringValue; }
        set { stringValue = value; }
    }
}

Using the following URL:

http://localhost:1122/Service1.svc/GetDataUsingDataContract?composite={"BoolValue":"True","StringValue":"Hello"}

With the Enpoint Configuration:

<system.serviceModel>
    <services>
        <service name="WebHTTPBindingExample.Service1" behaviorConfiguration="WebHTTPBindingExample.Service1Behavior">
            <host>
                <baseAddresses>
                    <add baseAddress="http://localhost:8731/Design_Time_Addresses/WebHTTPBindingExample/Service1/"/>
                </baseAddresses>
            </host>
            <!-- Service Endpoints -->
            <!-- Unless fully qualified, address is relative to base address supplied above -->
            <!--<endpoint address="" binding="wsHttpBinding" contract="WebHTTPBindingExample.IService1">
                --><!-- 
      Upon deployment, the following identity element should be removed or replaced to reflect the 
      identity under which the deployed service runs.  If removed, WCF will infer an appropriate identity 
      automatically.
  --><!--
                <identity>
                    <dns value="localhost"/>
                </identity>
            </endpoint>-->
            <endpoint address="Web" behaviorConfiguration="ChatAspNetAjaxBehavior" binding="webHttpBinding" name="ajaxEndpoint" contract="WebHTTPBindingExample.IService1"/>
            <!-- Metadata Endpoints -->
            <!-- The Metadata Exchange endpoint is used by the service to describe itself to clients. -->
            <!-- This endpoint does not use a secure binding and should be secured or removed before deployment -->
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
        </service>
    </services>
    <behaviors>
        <serviceBehaviors>
            <behavior name="WebHTTPBindingExample.Service1Behavior">
                <!-- To avoid disclosing metadata information, 
  set the value below to false and remove the metadata endpoint above before deployment -->
                <serviceMetadata httpGetEnabled="True"/>
                <!-- To receive exception details in faults for debugging purposes, 
  set the value below to true.  Set to false before deployment 
  to avoid disclosing exception information -->
                <serviceDebug includeExceptionDetailInFaults="False"/>
            </behavior>
        </serviceBehaviors>
        <endpointBehaviors>
            <behavior name="ChatAspNetAjaxBehavior">
                <webHttp/>
            </behavior>
        </endpointBehaviors>
    </behaviors>
</system.serviceModel>

I Get the follwoing Error:

Operation 'GetDataUsingDataContract' in contract 'IService1' has a query variable named 'composite' of type 'WebHTTPBindingExample.CompositeType', but type 'WebHTTPBindingExample.CompositeType' is not convertible by 'QueryStringConverter'.  Variables for UriTemplate query values must have types that can be converted by 'QueryStringConverter'.
like image 287
Boden Avatar asked Mar 09 '11 05:03

Boden


2 Answers

[NEW ANSWER (2019]

In case somebody is still searching for this (I just did, and I figured out a good solution).

In contract file:

[OperationContract]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json, UriTemplate = "TestBlockHeight?blockHeight={blockHeight}")]
Task<string> TestBlockHeight(BlockHeight blockHeight);

In service file:

public async Task<string> TestBlockHeight(BlockHeight blockHeight)
{
    return await Task.FromResult($"Called TestBlockHeight with parameter {blockHeight}");
}

My custom BlockHeight type class:

[TypeConverter(typeof(MyBlockHeightConverter))]
public class BlockHeight : IComparable<ulong>
{
    private ulong value;

    public BlockHeight(ulong blockHeight)
    {
        value = blockHeight;
    }
}

And the custom TypeConverter class that I needed to create:

public class MyBlockHeightConverter : TypeConverter
{
    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if(destinationType == typeof(string) && value is BlockHeight blockHeight)
        {
            return blockHeight.ToString();
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if(value is string str)
        {
            return new BlockHeight(ulong.Parse(str));
        }
        return base.ConvertFrom(context, culture, value);
    }

    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if(sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        if (destinationType == typeof(string))
        {
            return true;
        }
        return base.CanConvertTo(context, destinationType);
    }
}

This works because WCF uses a QueryStringConverter to serialize url parameters, and only specific types are allowed. One of these types is an arbitrary type which is decorated with TypeConverterAttribute. More info here: https://docs.microsoft.com/en-us/dotnet/api/system.servicemodel.dispatcher.querystringconverter?view=netframework-4.8

like image 57
alexpanter Avatar answered Oct 28 '22 04:10

alexpanter


I don't believe you're allowed to pass complex types on the query string using WCF this way. See this response from a Microsoft tech in the ASP.NET forums - it's similar to your situation.

Based on how you've stubbed out your service method, it seems like a more appropriate verb to use would be POST or PUT, and you could put your CompositeType payload in the request body. But I'm guessing as to your intention.

I was able to make your code work and still use a GET by changing the query string type from CompositeType to String and then deserializing the JSON string into a CompositeType by using the ASP.NET JavaScriptSerializer class. (You can use your favorite JSON helper class here -- I'm partial to JSON.NET, but I also hear FlexJson is very good, too.)

I didn't touch your web.config (except to make it work in my local test app). My only change was in the service method signature and the implementation of the service method.

[ServiceContract]
public interface IService1
{

    [OperationContract]
    [WebGet(UriTemplate = "GetDataUsingDataContract?composite={composite}",
        BodyStyle = WebMessageBodyStyle.Wrapped,
        RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    CompositeType GetDataUsingDataContract(String composite);

    // TODO: Add your service operations here
}


public class Service1 : IService1
{
    public CompositeType GetDataUsingDataContract(String composite)
    {
        //use the JavaScriptSerializer to convert the string to a CompositeType instance
        JavaScriptSerializer jscript = new JavaScriptSerializer();
        CompositeType newComp = jscript.Deserialize<CompositeType>(composite);
        newComp.StringValue += " NEW!";
        return newComp;
    }

}

I hope this helps. Let me know if you have other questions with this.

like image 24
David Hoerster Avatar answered Oct 28 '22 04:10

David Hoerster