I am having a little problem while sending data from my React app to my Spring Boot Controller, I am sending the data via a put method, but I get 400, error, and an error in eclipse pops up, so What I did is :
export const changeContratTypes = (idContrat, items, declaration) => {
const endpoint = template(CONTRAT_TYPES_CHANGE);
return instance // just an axios instance
.put(endpoint({ idContrat }), { items, declaration })
.then(values => values)
.catch(err => err.response);
};
My endpoint constant is the url, simple is that, and I send declaration which is an integer and items which is an array of object, my object structure is :
{
id: 1, // or 2, 3, ....
isSelected: true, // or false
title: "a String here"
}
To get this in Spring boot I created this method in my controller :
#CrossOrigin(origins = "*")
#ApiOperation(value = "${contrat.recuperation}", notes = "${contrat.recuperation.notes}", response = ContratDetailDto.class)
#PutMapping(value="/{idContrat}/trtype")
#ApiModelProperty(example = "4000004")
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Enrigistrer Les types de contrats ") })
public ResponseEntity enrigistrerTypesDeContrat(#ApiParam(value = "${contrat.recuperation.param.id}") #PathVariable long idContrat, #RequestBody TypesConformites tcf) {
if (log.isDebugEnabled()) {
log.debug("appel de la méthode enrigistrerTypesDeContrat");
}
System.out.println("Voila "+tcf.getDeclaration());
return ResponseEntity.ok(HttpStatus.OK);
}
This controller is well mapped and other methods in it works fine, but all methods I used are Get Methods.
What I did before that is creating a class used as a RequestBody :
#Getter #Setter
public class TypesConformites {
private int declaration;
private ArrayList<Item> items;
public TypesConformites() {
}
}
and Here is my Item class :
#Getter #Setter
public class Item {
private int id;
private String title;
private boolean isSelected;
public Item() {
}
}
I get this error in Java :
Blockquote
JSON parse error: Unrecognized field "isSelected" (class com.apicil.cosy.contrat.controller.api.impl.external.Item), not marked as ignorable; nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "isSelected" (class com.apicil.cosy.contrat.controller.api.impl.external.Item), not marked as ignorable (3 known properties: "title", "id", "selected"])
at [Source: (PushbackInputStream); line: 1, column: 66] (through reference chain: com.apicil.cosy.contrat.controller.api.impl.external.TypesConformites["items"]->java.util.ArrayList[0]->com.apicil.cosy.contrat.controller.api.impl.external.Item["isSelected"])
What's wrong with that code, Any help would be much appreciated.
Generally the Item is deserialised by jackson like this :-
public void setId(String firstName) {
public void setTitle(String lastName) {
public void setSelected(boolean isActive) {
To avoid this you can just changed the mapping name in the Item and request body.. or annotated your isSelected with #JsonProperty
Related
Goal
To protect the web app from malicious spam bot crawlers and similar malicious actors my goal is to use reCAPTCHA v3 to analyze the user visiting the site and if the Captcha v3 score is good enough (let's say 0.5 or better) use the Fetch API to POST the token and so verify it and if the score is good enough as mentioned previously return the E-Mail address within some HTML. For simplicity sake, the function loadContactbubble() gets executed when you click a button.
Problems
I am not sure where to implement the if (response.score => 0.5) check.
Frontend semi-works in that regard that in the network browser debug tools it gives an response but in the console it prints out the response as undefined
Is my implementation secure enough? Can't the secret key somehow siphoned or similar?
I get a lot of CSP warnings in the browser, might this be an issue in production?
Code
I was following this guide: https://dev.to/spencer741/google-recaptcha-v3-server-side-validation-using-asp-net-core-5-0-3hfb (that means that the code is like 80-90% from this article)
My appsettings.json contains the secret key and the siteverify Link (API link).
GHttpModels.cs:
using System;
using System.Runtime.Serialization;
namespace _projectname.Tooling
{
public class GRequestModel
{
public string path { get; set; }
public string secret { get; set; }
public string response { get; set; }
public string remoteip { get; set; }
public GRequestModel(string res, string remip)
{
response = res;
remoteip = remip;
secret = Startup.Configuration["GoogleRecaptchaV3:Secret"];
path = Startup.Configuration["GoogleRecaptchaV3:ApiUrl"];
if (String.IsNullOrWhiteSpace(secret) || String.IsNullOrWhiteSpace(path))
{
//Invoke logger
throw new Exception("Invalid 'Secret' or 'Path' properties in appsettings.json. Parent: GoogleRecaptchaV3.");
}
}
}
//Google's response property naming is
//embarrassingly inconsistent, that's why we have to
//use DataContract and DataMember attributes,
//so we can bind the class from properties that have
//naming where a C# variable by that name would be
//against the language specifications... (i.e., '-').
[DataContract]
public class GResponseModel
{
[DataMember]
public bool success { get; set; }
[DataMember]
public string challenge_ts { get; set; }
[DataMember]
public string hostname { get; set; }
//Could create a child object for
//error-codes
[DataMember(Name = "error-codes")]
public string[] error_codes { get; set; }
}
}
GoogleReCaptchaV3Service.cs:
using System;
using System.Threading.Tasks;
using System.Text.Json;
using System.Web;
using System.Net.Http;
using System.IO;
using System.Text;
using System.Runtime.Serialization.Json;
namespace _projectname.Tooling
{
public class CaptchaRequestException : Exception
{
public CaptchaRequestException()
{
}
public CaptchaRequestException(string message)
: base(message)
{
}
public CaptchaRequestException(string message, Exception inner)
: base(message, inner)
{
}
}
public interface IGoogleRecaptchaV3Service
{
HttpClient _httpClient { get; set; }
GRequestModel Request { get; set; }
GResponseModel Response { get; set; }
void InitializeRequest(GRequestModel request);
Task<bool> Execute();
}
public class GoogleRecaptchaV3Service : IGoogleRecaptchaV3Service
{
public HttpClient _httpClient { get; set; }
public GRequestModel Request { get; set; }
public GResponseModel Response { get; set; }
public HttpRequestException HttpReqException { get; set; }
public Exception GeneralException { get; set; }
public GoogleRecaptchaV3Service(HttpClient httpClient)
{
_httpClient = httpClient;
}
public void InitializeRequest(GRequestModel request)
{
Request = request;
}
public async Task<bool> Execute()
{
// Notes on error handling:
// Google will pass back a 200 Status Ok response if no network or server errors occur.
// If there are errors in on the "business" level, they will be coded in an array;
// CaptchaRequestException is for these types of errors.
// CaptchaRequestException and multiple catches are used to help seperate the concerns of
// a) an HttpRequest 400+ status code
// b) an error at the "business" level
// c) an unpredicted error that can only be handled generically.
// It might be worthwhile to implement a "user error message" property in this class so the
// calling procedure can decide what, if anything besides a server error, to return to the
// client and any client handling from there on.
try
{
//Don't to forget to invoke any loggers in the logic below.
//formulate request
string builtURL = Request.path + '?' + HttpUtility.UrlPathEncode($"secret={Request.secret}&response={Request.response}&remoteip={Request.remoteip}");
StringContent content = new StringContent(builtURL);
Console.WriteLine($"Sent Request {builtURL}");
//send request, await.
HttpResponseMessage response = await _httpClient.PostAsync(builtURL, null);
response.EnsureSuccessStatusCode();
//read response
byte[] res = await response.Content.ReadAsByteArrayAsync();
string logres = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Retrieved Response: {logres}");
//Serialize into GReponse type
using (MemoryStream ms = new MemoryStream(res))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(GResponseModel));
Response = (GResponseModel)serializer.ReadObject(ms);
}
//check if business success
if (!Response.success)
{
throw new CaptchaRequestException();
}
//return bool.
return true; //response.IsSuccessStatusCode; <- don't need this. EnsureSuccessStatusCode is now in play.
}
catch (HttpRequestException hre)
{
//handle http error code.
HttpReqException = hre;
//invoke logger accordingly
//only returning bool. It is ultimately up to the calling procedure
//to decide what data it wants from the Service.
return false;
}
catch (CaptchaRequestException ex)
{
//Business-level error... values are accessible in error-codes array.
//this catch block mainly serves for logging purposes.
/* Here are the possible "business" level codes:
missing-input-secret The secret parameter is missing.
invalid-input-secret The secret parameter is invalid or malformed.
missing-input-response The response parameter is missing.
invalid-input-response The response parameter is invalid or malformed.
bad-request The request is invalid or malformed.
timeout-or-duplicate The response is no longer valid: either is too old or has been used previously.
*/
//invoke logger accordingly
//only returning bool. It is ultimately up to the calling procedure
//to decide what data it wants from the Service.
return false;
}
catch (Exception ex)
{
// Generic unpredictable error
GeneralException = ex;
// invoke logger accordingly
//only returning bool. It is ultimately up to the calling procedure
//to decide what data it wants from the Service.
return false;
}
}
}
}
Startup.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
//from captchav3
using _projectname.Tooling;
namespace _projectname
{
public class CookieCheckMiddleware
{
private readonly RequestDelegate _next;
public CookieCheckMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Cookies["ModalShown"] == null && httpContext.Request.Path != "/Cookies")
{
httpContext.Response.Redirect("/Cookies?q="+ httpContext.Request.Path);
}
await _next(httpContext); // calling next middleware
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class CookieCheckMiddlewareExtensions
{
public static IApplicationBuilder UseCookieCheckMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CookieCheckMiddleware>();
}
}
public class Startup
{
internal static IConfiguration Configuration { get; private set; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
//public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Captcha v3
services.AddHttpClient<IGoogleRecaptchaV3Service, GoogleRecaptchaV3Service>();
services.AddTransient<IGoogleRecaptchaV3Service, GoogleRecaptchaV3Service>();
services.AddControllers();
//Register dependencies
services.AddRazorPages();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//app.Use(async (ctx, next) =>
//{
// await next();
// if (ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
// {
// //Re-execute the request so the user gets the error page
// string originalPath = ctx.Request.Path.Value;
// ctx.Items["originalPath"] = originalPath;
// ctx.Request.Path = "/Cloud";
// await next();
// }
//});
// orig
//app.UseExceptionHandler("/Errors/{0}");
app.UseStatusCodePagesWithRedirects("/Errors/{0}");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// TEST
app.UseCookieCheckMiddleware();
app.UseAuthentication();
app.UseAuthorization();
var cookiePolicyOptions = new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict,
};
app.UseCookiePolicy(cookiePolicyOptions);
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
// Experimental
endpoints.MapControllers();
});
}
}
}
_Layout.cshtml:
<button onclick="loadContactbubble();">Load contacts</button>
site.js (only the function for brevity):
function loadContactbubble() {
grecaptcha.execute('sitekeyhere', { action: 'onclick' }).then(function (token) {
console.log(token);
fetch("/load/contactbubble?RecaptchaToken=" + token, {
method: "POST",
body: token,
})
}).then((response) => {
console.log(response);
if (!response.ok) {
const errorBuild = {
type: "Error",
message: response.message || "Something went wrong",
data: response.data || "",
code: response.code || "",
};
}
}
//addText("Error: " + JSON.stringify(errorBuild));
//toggleLoader(2, "hidden");
//return;
)
}
ApiController.cs:
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using _projectname.Tooling;
using System.Threading.Tasks;
namespace _projectname.Controllers
{
public class SignUpModel
{
public string RecaptchaToken { get; set; }
}
[ApiController]
[Route("load/contactbubble")]
public class SignUp : ControllerBase
{
IGoogleRecaptchaV3Service _gService { get; set; }
public SignUp(IGoogleRecaptchaV3Service gService)
{
_gService = gService;
}
[HttpPost]
public async Task<IActionResult> Post([FromQuery] SignUpModel SignUpData)
{
GRequestModel rm = new GRequestModel(SignUpData.RecaptchaToken,
HttpContext.Connection.RemoteIpAddress.ToString());
_gService.InitializeRequest(rm);
if (!await _gService.Execute())
{
//return error codes string.
return Ok(_gService.Response.error_codes);
}
//call Business layer
//return result
return base.Content("<div>Welcome human! Here is our secret e-mail: test#test.com</div>", "text/html");
}
}
}
Errors
If I click the button the following gets printed out in the console:
Uncaught (in promise) TypeError: can't access property "ok", response is undefined
The response contains proper HTML in the network tab, which is weird.
How do I fix this?
Your function
function (token) {
console.log(token);
fetch("/load/contactbubble?RecaptchaToken=" + token, {
method: "POST",
body: token,
});
}
does not return anything, hence why the argument passed to the next .then((response) => ... is undefined.
Make this function return the fetched data, and it should hopefully work:
function (token) {
console.log(token);
return fetch("/load/contactbubble?RecaptchaToken=" + token, {
method: "POST",
body: token
});
}
(Well, it should then at least forward the fetch result to the next .then((response) => .... I have not looked for other errors in your code, so "it should hopefully work" is to be understood with respect to the one problem i explained here...)
I have a class like this and need to create a collection of properties .in c# i can use a list or any collection type but how to do it in typescript .
class Movies implements Imovies{
public Id : string;
public get fid(): string {
return this.Id;
}
public set fid(theid:string) {
this.Id=theid;
}
public dailyrentalRate : number;
public get fdailyrentalRate(): number {
return this.dailyrentalRate;
}
public set fdailyrentalRate(theid:number) {
this.dailyrentalRate=theid;
}
public numberinstock : number;
public get fnumberinstock(): number {
return this.numberinstock;
}
public set fnumberinstock(theid:number) {
this.numberinstock=theid;
}
public publishDate : string;
public get fpublishDate(): string {
return this.publishDate;
}
public set fpublishDate(theid:string) {
this.publishDate=theid;
}
public title : string;
public get ftitle(): string {
return this.title;
}
public set ftitle(theid:string) {
this.title=theid;
}
}
I tried to insert values to array as follows
let mov=new Array<Movies>();
mov[0].fid="asdasd";
mov[0].fdailyrentalRate=2;
mov[0].fnumberinstock=5;
mov[0].fpublishDate="2018-05-23";
mov[0].ftitle="name";
mov[1].fid="asdasda";
mov[1].fdailyrentalRate=32;
mov[1].fnumberinstock=35;
mov[1].fpublishDate="2018-06-23";
mov[1].ftitle="name2";
but is showing an error
TypeError: Cannot set property 'fid' of undefined
you can also implement the class Movie using a constructor, and then create an array of Movie, like this:
const movies: Movie[] = [];
after you could push movie objects in the movies array:
const movie = new Movie();
movie.fid="test1";
movie.dailyrentalRate=2;
movie.numberinstock=5;
movie.publishDate="2018-05-23";
movie.title="name";
movies.push(movie);
you can see a working example here: https://stackblitz.com/edit/typescript-array-movies
I am new to json and angular. I am trying to access an API response using model. But it is giving me undefined when I try to access it.
Below is the json API returns
{
"Inventory App": {
"AnalyticsUI": "UP",
"BaseUI": "UP",
"PlanningUI": "UP",
"UploadUI": "DOWN"
}
}
My model definition is below
export class AppModel {
constructor(
public experience: AppList
) {}
}
export class AppList {
constructor(
public appName1: String,
public appName2: String,
public appName3: String,
public appName4: String,
public appName5: String
) {}
}
Below is my service call
import { AppModel } from './model/appList.model';
getAppStatus$(): Observable<AppModel> {
return this.http
.get('https://abc.xyc.com/AppController/AppsStatus')
.catch(this._handleError);
}
Below is the component where I am trying to access the API data.
export class MainComponent {
......
appList: AppModel;
.....
public _getAppStatus() {
this.appSub = this.api
.getAppStatus$()
.subscribe(
res => {
this.appList = res;
console.log(this.appList);
console.log(this.appList.experience);
},
err => {console.error(err); }
);
}
}
It is giving me undefined when I try to access this.appList.experience. Where as this.appList is printing the json result properly. Any help on this is much appreciated.
Console output :
Console output
As AJT and Khan stated, issue was property name not matching. Once I changed the property name to match the json response, I was able to get the values.
I have the following method in a service I've created:
getPost(nid: string): Observable<Post[]>{
let url = "http://test.co.uk/api/v1/basic/" + nid;
return this.http.get(url, {headers: this.headers}).map(res => res.json() as Post).catch(err => {
return Observable.throw(err);
});
}
And this is the class of my component:
export class PostDetailComponent implements OnInit {
posts: Post[] = [];
post: Post = new Post();
constructor(
private route: ActivatedRoute,
private postService: PostService
) { }
ngOnInit() {
this.route.params.switchMap((params: Params) => {
let nid = params ['nid'];
return this.postService.getPost(nid); }).subscribe(res => {
console.log(res)
this.post = res as Post;
}, err =>{
console.log(err);
});
}
}
The JSON feed looks like this(yes one object in the array):
[
{
"nid":"3",
"title":"When Unity meets Vuforia",
"body":"<p>Unless you have been living under a rock in the past 7 - ...",
"uid":"admin",
"path":"\/node\/3",
"field_article_image":"http:\/\/test.co.uk\/sites\/default\/files\/when-unity-meets-vuforia_0.jpg?itok=BGYaotay"
}
]
So in my template, if I print {{post}} I get [object Object] on the screen.
If I print {{post | json}} I get the row JSON feed.
And finally, if I print {{post.title}} or {{post?.title}} I don't get anything.
I also have a class Post that is looking like this:
export class Post{
constructor(
public nid?: string,
public title?: string,
public body?: string
public image?: string
){
}
}
Any hints?
You are assigning an array into what should be a single object. Copy the first element of the array into the post variable
this.post = res[0] as Post
Side note: It's incorrect to assign a raw object to a class instance. In this case, your this.post.constructor won't exist and this.post instanceof Post == false.
You could do Object.assign(this.post, res[0]) but you may need to clear existing properties if not all properties are always present.
I prefer to define object shapes as interfaces instead, then you would not have that problem because all the interface information is removed at runtime, whereas a class does emit some code instead of just doing static type checks at compilation time
I'm having a class like the following:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
[DataContract()]
public class TestCol : List<Test> { }
[DataContract()]
public class MainTest
{
public TestCol Components { get; set; }
}
[DataContract()]
public class Test
{
public Test() { }
public String Name { get; set; }
}
And a webservice with the following webmethod like this:
[WebMethod]
public String Test(MainTest input)
{
String rtrn = String.Empty;
foreach (Test test in input.Components)
rtrn += test.Name;
return rtrn;
}
Which is called by AJAX with the following method:
var Test = {};
Test.Name = "Test";
var MainTest = {};
MainTest.Components = [];
MainTest.Components.push(Test);
$.ajax({
type: "POST",
url: "WebService/WSTest.asmx/Test",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({
"input": MainTest
}),
success: function(data, textStatus) {
console.log("success");
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
window.console && console.log && console.log(XMLHttpRequest.responseText + " || " + textStatus + " || " + errorThrown);
}
});
When executing the AJAX call, it will return errors. I found out that the error is with the typed class TestCol, which has no properties.
Now do I have found 2 solutions that require changes in the C# classes:
Remove the TestCol class and change the Components property to List<Test> datatype:
[DataContract()]
public class MainTest
{
public List<Test> Components { get; set; }
}
[DataContract()]
public class Test
{
public Test() { }
public String Name { get; set; }
}
Or add an extra property to the TestCol class and change the webmethod:
[DataContract()]
public class TestCol : List<Test>
{
public List<Test> Components { get; set; }
}
[DataContract()]
public class MainTest
{
public TestCol Components { get; set; }
}
[DataContract()]
public class Test
{
public Test() { }
public String Name { get; set; }
}
&
[WebMethod]
public String Test(MainTest input)
{
String rtrn = String.Empty;
foreach (Test test in input.Components.Components)
rtrn += test.Name;
return rtrn;
}
Both solutions require changes in the C# classes, which I prefer not to, as other code is depended on it. Does anyone know a solution for this problem?
Edit: I've uploaded a test solution, containing above code: http://jeroenvanwarmerdam.nl/content/temp/JSONtoClassWebservice.zip
So this solution changes the List to Object instead of Test. I hoped to change as little code as possible (i dislike having to do casts in foreach loops). The below code does so with two function additions and the previously mentioned inheritance change.
public class TestCol : List<object>
{
public new IEnumerator<Test> GetEnumerator()
{
return this.ConvertAll<Test>(
dict => ConvertDictionaryTo<Test>(
(Dictionary<string, object>) dict
)
).GetEnumerator();
}
private T ConvertDictionaryTo<T>(IDictionary<string, object> dictionary) where T : new()
{
Type type = typeof(T);
T ret = new T();
foreach (var keyValue in dictionary)
{
type.GetProperty(keyValue.Key).SetValue(ret, keyValue.Value, null);
}
return ret;
}
}
Convert function courtesy TurBas
Mapping object to dictionary and vice versa
JavaScriptSerializer serialization: IEnumerable -> JavaScript Array
When the JavaScriptSerializer is used, it automatically converts an IEnumerable (without IDictionary) type -- that covers List<> or anything derived from it -- into an array.
Deserialization: JavaScript Array -> IEnumerable -> Collection Object
Now, upon deserialization from JSON, the JavaScriptSerializer must take the array, create an IEnumerable, then create an object for the field by passing that IEnumerable into its constructor.
Constructing Collection object via Constructor
Now, for List<> you have a constructor overload that takes an IEnumerable. So if you put List<Test> as the type of your component it creates it fine.
Constructors not inherited
However, TestCol does NOT have such a constructor! The reason why it worked with List<Test> and not with TestCol (which derives from List<Test>) is that the only thing that is not inherited between classes are constructors!
Therefore, the JavaScriptSerializer does not have any way to construct a TestCol from an IEnumerable. So it fails silently.
Deserialize Array by Creating List, then Casting to Type
Now the JavaScriptSerializer may then attempt to create a List<Test> from this IEnumerable<Test>, and then try to cast it into a TestCol.
Possible Solution
Solution: Try putting in:
public TestCol () {} // Need this when you have another constructor
public TestCol (IEnumerable<Test> list) : base(list) {} // Constructor that takes an IEnumerable
public TestCol (IList<Test> list) : base(list) {} // Constructor that takes an IList
as your TestCol's constructors.
And if it still doesn't work, implement an explicit type cast from List<Test> to TestCol.
public static explicit operator TestCol(IList<Test> list) { return new TestCol(list); }
hmmm this didn't work in the web method?
foreach (Test test in input.Components.TestCol)
Re comment below, does this work then?
foreach (Test test in (List<Test>)input.Components.TestCol)
It should work because a class can be enumerated...
If you're expecting JSON, you'll need to return JSON.
Check with the System.Web.Script.Serialization.JavaScriptSerializer
http://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptserializer.aspx
If you use ASMX services the JavaScriptSerializer will be responsible for the data conversion and not the DataContractJsonSerializer. So all DataContract attributes which you use will not work.
You are write that classes like public class TestCol : List<Test> { } are bad for the JavaScriptSerializer, but classes having List<Test> as the property (public class MainTest { public List<Test> Components { get; set; }}) have no problem.
So I suggest to simplify you code to the following. The classes used as the parameters can be defines as
public class Test {
public String Name { get; set; }
}
public class MainTest {
public List<Test> Components { get; set; }
}
The WebMethod Test will be
[WebMethod]
public String Test(MainTest input)
{
StringBuilder rtrn = new StringBuilder();
foreach (Test test in input.Components) {
rtrn.AppendLine (test.Name);
}
return rtrn.ToString ();
}
and the ajax call can be
var tests = {
Components: [
{Name:"Test1"},
{Name:"Test2"},
{Name:"Test3"}
]
};
$.ajax({
type: "POST",
url: "WebService1.asmx/Test",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({
"input": tests
}),
success: function (data, textStatus) {
alert("success:\n" + data.d);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(XMLHttpRequest.responseText+" || "+textStatus+" || "+errorThrown);
}
});
How you can see all will be very simple and it's work. For more details how you can send complex data I recommend you to read another answer and this.
You seem to be using ASMX (not WCF) because you have omitted [DataMember] attributes on all your public properties and still get serialized. WCF is "opt-in", so you shouldn't be seeing any serialization of any properly.
As a result, all [DataContract] attributes are useless.
ASMX defaults to the JavaScriptSerializer if you are using ScriptManger and outputing JSON. The JavaScriptSerializer is "opt-out" (which means that all public properties are automatically serialized unless marked with [ScriptIgnoreAttribute]).
The JavaScriptSerializer supports serializing List<>'s. You should not be having problems serializing your TestCol property because JavaScriptSerializer automatically supports serializing all types that implement IEnumerable (but not IDictionary) -- which includes List<> -- into JSON arrays.
Your error seems to be that the JavaScriptSerializer does not properly handle classes that inherit from List<> (or from a class implementing IEnumerable). In your first work-around, you eliminated the class that inherited from List<>. In your second work-around, you skipped all functionalities of the base class, but re-implemented the List<> in a property.
Your JSON post data currently looks like:
{ Components: [
{ Name:"foo" },
{ Name:"bar" },
:
] }
However, you have one extra level or redirection in the serializer (inheriting from List<Test> -> TestCol). It is possible that the serializer is looking for:
{ Components: {
Items: [
{ Name:"foo" },
{ Name:"bar" },
:
] }
}
because you are essentially serializing the "Items" property of List<>. So, your JSON post data is just feeding Test objects to the wrong place, and your TestCol Components property ends up empty.
I'd suggest that you add a web service method to output a test MainTest object to see what it serializes into. You'll probably find that it puts in an additional level.