I'm doing some tests with WebAPI
, to see how it behaves with circular reference, and I found a curious case.
Model
[DataContract(IsReference = true)]
public class Pessoa
{
[DataMember(EmitDefaultValue = false, Order = 1)]
public string Nome { get; set; }
[DataMember(EmitDefaultValue = false, Order = 2)]
public Pessoa Patner { get; set; }
[DataMember(EmitDefaultValue = false, Order = 3)]
public Pessoa Father { get; set; }
[DataMember(EmitDefaultValue = false, Order = 4)]
public Pessoa Mother { get; set; }
[DataMember(EmitDefaultValue = false, Order = 5)]
public Pessoa[] Children { get; set; }
[DataMember(EmitDefaultValue = false, Order = 6)]
public Pessoa[] Siblings { get; set; }
public Pessoa(string nome)
{
this.Nome = nome;
}
}
ApiController
public async Task<Pessoa[]> ListarFamilia()
{
var father = new Pessoa("Person 01");
var mother = new Pessoa("Person 02");
var child1 = new Pessoa("Person 03");
var child2 = new Pessoa("Person 04");
var child3 = new Pessoa("Person 05");
father.Patner = mother;
father.Children = new Pessoa[] { child1, child2, child3 };
mother.Patner = father;
mother.Children = new Pessoa[] { child1, child2, child3 };
child1.Father = father;
child1.Mother = mother;
child1.Siblings = new Pessoa[] { child2, child3 };
child2.Father = father;
child2.Mother = mother;
child2.Siblings = new Pessoa[] { child1, child3 };
child3.Father = father;
child3.Mother = mother;
child3.Siblings = new Pessoa[] { child1, child2 };
var familia = new Pessoa[] { father, mother, child1, child2, child3 };
return familia;
}
public async Task<int> ContarPessoas(Pessoa[] familia)
{
return familia.Length;
}
Global.asax
GlobalConfiguration.Configure(WebApiConfig.Register);
Newtonsoft.Json.JsonConvert.DefaultSettings = () =>
{
return new Newtonsoft.Json.JsonSerializerSettings
{
ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize,
PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects
};
};
When I call the method in /api/Pessoa/ListarFamilia
, it returns me the following JSON
[
{
"Nome": "Person 01",
"Patner": {
"Nome": "Person 02",
"Patner": {
"$ref": 1
},
"Children": [
{
"Nome": "Person 03",
"Father": {
"$ref": 1
},
"Mother": {
"$ref": 2
},
"Siblings": [
{
"Nome": "Person 04",
"Father": {
"$ref": 1
},
"Mother": {
"$ref": 2
},
"Siblings": [
{
"$ref": 3
},
{
"Nome": "Person 05",
"Father": {
"$ref": 1
},
"Mother": {
"$ref": 2
},
"Siblings": [
{
"$ref": 3
},
{
"$ref": 4
}
],
"$id": 5
}
],
"$id": 4
},
{
"$ref": 5
}
],
"$id": 3
},
{
"$ref": 4
},
{
"$ref": 5
}
],
"$id": 2
},
"Children": [
{
"$ref": 3
},
{
"$ref": 4
},
{
"$ref": 5
}
],
"$id": 1
},
{
"$ref": 2
},
{
"$ref": 3
},
{
"$ref": 4
},
{
"$ref": 5
}
]
So far so good, but when I try to send this same JSON to /api/Pessoa/ContarPessoas
, the family object is not completely deserialized, since Formatter can not handle the "$ ref".
Finally, it overrides the ContarPessoas
method
public async Task<int> ContarPessoas(object request)
{
var jsonStr = request.ToString();
var familia = JsonConvert.DeserializeObject<Pessoa[]>(jsonStr);
return familia.Length;
}
The request arrived correctly, with all { '$ref': 'n' }
in their respective places, but DeserializeObject does not recognize them.
Finally, this is the script I'm using on my page html
:
<script type="text/javascript">
JSON.decircle = function (obj) {
JSON._decircle(obj, []);
}
JSON.recircle = function (obj) {
var refs = [];
var objs = [];
JSON._recircleScanIds(obj, refs);
JSON._recircleScanRefs(obj, objs);
for (var indice in objs) {
var obj = objs[indice].obj;
var key = objs[indice].key;
var end = obj[key]["$ref"];
obj[key] = refs[end]
}
}
JSON._decircleCheck = function (obj, key, refs) {
var child = obj[key];
if (child && typeof child === "object") {
if (Array.isArray(child)) {
for (var indice in child) {
JSON._decircleCheck(child, indice, refs)
}
} else {
if (refs.indexOf(child) == -1) {
JSON._decircle(child, refs);
} else {
obj[key] = { "$ref": child["$id"] };
}
}
}
}
JSON._decircle = function (obj, refs) {
if (obj && typeof obj === "object") {
if (Array.isArray(obj)) {
for (var key in obj) {
JSON._decircleCheck(obj, key, refs);
}
} else {
if (refs.indexOf(obj) == -1) {
refs.push(obj);
obj["$id"] = "" + refs.length;
for (var key in obj) {
JSON._decircleCheck(obj, key, refs);
}
}
}
}
}
JSON._recircleScanIds = function (obj, refs) {
if (obj && typeof obj === "object") {
if (Array.isArray(obj)) {
for (var key in obj) {
JSON._recircleScanIds(obj[key], refs);
}
} else {
var id = obj["$id"];
if (id) {
refs[id] = obj;
delete obj["$id"];
}
for (var key in obj) {
JSON._recircleScanIds(obj[key], refs);
}
}
}
}
JSON._recircleScanRefs = function (obj, objs) {
if (obj && typeof obj === "object") {
for (var key in obj) {
var child = obj[key];
var isArray = Array.isArray(child);
if (child && typeof child === "object") {
if (!isArray && child["$ref"]) {
objs.push({ obj: obj, key: key });
} else {
JSON._recircleScanRefs(child, objs);
}
}
}
}
}
var getFamily = function (callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.open("POST", "http://localhost:54454/api/Pessoa/ListarFamilia", true);
httpRequest.responseType = "json";
httpRequest.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
httpRequest.addEventListener("readystatechange", function (event) {
if (httpRequest.readyState == 4) {
var familia = httpRequest.response;
JSON.recircle(familia);
callback(familia);
}
})
httpRequest.send();
}
var sendFamily = function (familia, callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.open("POST", "http://localhost:54454/api/Pessoa/ContarPessoas", true);
httpRequest.responseType = "json";
httpRequest.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
httpRequest.addEventListener("readystatechange", function (event) {
if (httpRequest.readyState == 4) {
var quantidade = httpRequest.response;
callback(quantidade);
}
});
JSON.decircle(familia);
httpRequest.send(JSON.stringify(familia));
}
getFamily(function (familia) {
sendFamily(familia, function (quantidade) {
console.log(quantidade);
});
})
</script>