Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON array type resolution in ASP.Net webservices

So I have worked out how to pass my custom objects into ASP.Net json webservices. Works a charm.

Problem I am having is passing in straight arrays of my custom objects or alternatively passing in arrays that are parameters of my custom objects.

So for example...

Public Class WebService1
    Inherits System.Web.Services.WebService

    <WebMethod()> _
    <ScriptMethod(ResponseFormat:=ResponseFormat.Json)> _
    Public Function AddPersonList(ByVal PersonList As PersonList) As String
        Debug.Assert(False)
    End Function

    Public Class Person
        Public Sub New()
        End Sub

        Public Property FirstName As String
        Public Property LastName As String
    End Class

    Public Class PersonList
        Inherits List(Of Person)

    End Class
End Class

<script>
        $(function () {
            $.ajax({
                type: "POST",
                url: "WebService1.asmx/AddPersonList",
                data: " { PersonList: [ { FirstName: 'Max', LastName: 'Gershkovich' }, { FirstName: 'Test1', LastName: 'Test2' } ] }",
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                success: function (e) { debugger; },
                error: function (e) { debugger; }
            });
        });
    </script>

Fails with error: The value \"System.Collections.Generic.Dictionary`2[System.String,System.Object]\" is not of type \"WebApplication1.WebService1+Person\" and cannot be used in this generic collection.\r\nParameter name: value

So how do I explain to ASP.net that it is infact an array of Person?

Please note: That changing the function to as List(of Person) or ArrayList does work but given that I implement my own custom collections this is not optimal for me.

UPDATE: Ok so what I have worked out so far is that this problem is definitely associated with how the JavascriptSerializer uses the SimpleTypeResolver to resolve types. Basically if I do something like this

Public Function AddPersonList(ByVal PersonList As String) As PersonList

I can recreate the error using the following code.

Dim PersonList As PersonList = jsonSerializer.Deserialize(Of PersonList)(PList)

However when I provide my own custom type resolver along the lines of

Dim jsonSerializer As New JavaScriptSerializer(New MyCustomTypeResolver)

I can successfully create an instance of my custom list.

Now I have worked out how to provide my own custom convertor in the web.config file. Along the lines of this….

<system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization>
          <converters>
            <add name="MyCustomConverter" type="WebApplication1.MyCustomConverter" />
          </converters>
        </jsonSerialization>
      </webServices>      
    </scripting>
  </system.web.extensions>

But the problem is that I can’t find a way to specify a CustomTypeResolver. My assumption is that this is the only piece of the puzzle I am missing.

PSS: I have tried to use the assembly fully qualified name as specified in the docs for the SimpleTypeResolver (SimpleTypeResolver MSDN) but this throws a "Operation is not valid due to the current state of the object." exception - which is an error caused when the TypeResolver cannot resolve the name (I know this by testing with my CustomTypeResolver)

UPDATE @ Oleg and Sean:

I sincerely appreciate your input and have infact changed my application to reflect your suggestions; having said that the problem with this change is that it requires reworking of my class implementation and its associated behaviour in ways that I would have preferred to avoid.

My problem was never passing in a List(of Object) into my webservice (I simply posed the question as such to simplify it for stackoverflow). In such a case I would be willing to completely accept using a Generic List(of) but my problem was actually that one of my custom objects implemented a property with a strong typed List(of) so for example:

Customer {CustomerID AS Guid, FirstName AS String, LastName AS String, EmailAddresses AS EmailAddressList}

which now needs to be changed to

Customer {CustomerID AS Guid, FirstName AS String, LastName AS String, EmailAddresses AS List(Of EmailAddress)} 

This is admittedly not the end of the world and is probably better in the webservice context (as you have suggested) but definitely a detriment when it comes to internal application usage of my Collection properties. What this means is that once I have this property I either need to cast it to my CustomCollectionList every time I want to use some advanced feature or I need to implement another property which exposes the CustomCollectionList. Either solution leaves a nasty taste in my mouth.

like image 788
Maxim Gershkovich Avatar asked Jan 06 '11 05:01

Maxim Gershkovich


1 Answers

First of all the data

{ PersonList: [ { FirstName: 'Max', LastName: 'Gershkovich' }, { FirstName: 'Test1', LastName: 'Test2' } ] }

which you send to the web service are wrong JSON data. You can verify the data on http://www.jsonlint.com/. Correct JSON data will be

{"PersonList":[{"FirstName":"Max","LastName":"Gershkovich"},{"FirstName":"Test1","LastName":"Test2"}]}

Moreover I recommend you never make JSON manually. Instead of that you should use JSON.stringify. In the most cases the function are even natively supported in the web browsers (sometime after updates like here). Correct serialization of Web service parameters can be

$.ajax({
    data: {PersonList:JSON.stringify(t)} 
    // other parameters
});

(see here for more details) or

$.ajax({
    data: JSON.stringify({ PersonList: t })
    // other parameters
});

where

var t = [ { FirstName: 'Max', LastName: 'Gershkovich' },
          { FirstName: 'Test1', LastName: 'Test2' } ];

Which version of data representation JSON.stringify({ PersonList: t }) or {PersonList:JSON.stringify(t)} is correct depend on other things. It is easy to test which from there work in your environment.

Next small problem: you should better use List(Of Person) directly in the parameters of AddPersonList instead of the usage of types PersonList inherited from List(Of Person).

UPDATED: Just now I read your comments about List(Of Person) or PersonList to another answer. To prevent the same discussion I decide to write my opinion about this. It is not important which classes you use inside of your web service. You should design the interface of the web service so that it should be simple and clear for every person who knows nothing about your implementation. The input data of the method (at least what you included in the question) can be perfectly described with List(Of Person). Moreover List(Of Person) it is good for the data transfer. Inside of the method implementation you can convert List(Of Person) to any other classes which are good for the method internals. For example, it is very simple to construct PersonList from the List(Of Person). So I recommend you to follow the way: keep interface of the web service free from the implementation details.

UPDATED 2: It is not a problem at all if the object, which is the input parameter of the web method, has as the property another objects like List(Of EmailAddress). For example, if you defines EmailAddress from your last example as

Public Class EmailAddress
    Public Property Type As String
    Public Property Address As String
End Class

and Customer like

Public Class Customer
    'Public CustomerID As Guid
    Public Property FirstName As String
    Public Property LastName As String
    Public Property EmailAddresses As List(Of EmailAddress)
End Class

then you will have no problem with AddPersonList web method defined as following

<WebMethod()> _
Public Function AddPersonList(ByVal PersonList As List(Of Customer)) As String

In the same way you can use

<WebMethod()> _
Public Function AddPersonList1(ByVal input As InputData) As String

with

Public Class InputData
    Public Property PersonList As List(Of Customer)
End Class

or many other versions of object to send the information to the web server using $.ajax.

On the client side you can define myData as following

var myData = [
        { FirstName: 'Max', LastName: 'Gershkovich',
            EmailAddresses: [
                { Type: 'SMTP', Address: '[email protected]' },
                { Type: 'SMTP', Address: '[email protected]' },
            ]
        },
        { FirstName: 'Test1', LastName: 'Test2' }
    ];

and then send the data to the web server with

$.ajax({
    type: "POST",
    url: "WebService1.asmx/AddPersonList",
    data: JSON.stringify({ PersonList: myData }),
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (data) {
        alert(data.d);
    },
    error: function (xhr, textStatus, errorThrown) {
        alert("Error Occured!" + " | " + xhr.responseText + " | " +
              textStatus + " | " + errorThrown);
    }
});

or

$.ajax({
    type: "POST",
    url: "WebService1.asmx/AddPersonList1",
    data: JSON.stringify({ input: {PersonList: myData} }),
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (data) {
        alert(data.d);
    },
    error: function (xhr, textStatus, errorThrown) {
        alert("Error Occured!" + " | " + xhr.responseText + " | " +
              textStatus + " | " + errorThrown);
    }
});

All the above work without any problem. You can download the test example, which do this (Default.htm should be started. To call the method you should click on the first or the second button).

If you look at the myData object defined in the client code and then look at the usage of JSON.stringify above you will see that you will have no problem in sending any complex objects including arrays and properties. On the server side you can use List(Of Customer), List(Of EmailAddress) or list of other objects as the representations of JavaScript Arrays. All will work. Only your original example with the object inheritance is a bad one.

If you will try to design the interface of the web service looking from the client side and not from the internal server structures you will easy construct the classes for the corresponding input parameters of the web methods. And all will work immediately. If you will need to initialize your internal classes with the information you will be able to do it very simply. Such kind of data conversion will be very easy and you will not need to write any CustomTypeResolver which do in fact the same.

like image 161
Oleg Avatar answered Sep 28 '22 04:09

Oleg