JSON Serialization Issue with OData and Self-Referencing Complex Types
@ElizabethOkerio is already working on this.
Since Sep 19, 2023.
- Dominant language
- C#
- Stars
- 505
- Forks
- 186
- PR merge metrics
- No merged PRs in 30d
Description
Assemblies affected
Microsoft.AspNetCore.OData 8.2.3
Microsoft.Azure.Cosmos 3.35.4
Framework
dotnet 6
Dependencies
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.22" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="8.2.3" />
<PackageReference Include="Microsoft.AspNetCore.OData.NewtonsoftJson" Version="8.2.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5"/>
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.35.4" />
Describe the bug
A JsonSerializationException is occurring due to the detection of a self-referencing loop during JSON serialization. This issue is specifically related to the DeclaringType property of type Microsoft.OData.Edm.EdmComplexType within the object structure.
When the Cosmos SDK attempts to parse and serialize the EdmModel, it gets stuck in a referenced loop caused by the DeclaringType field.
Various attempts have been made to resolve the issue, including adding AddODataNewtonsoftJson as mentioned in issues #749 and #774. However, these attempts have not changed the defined serialization behavior in the SDK.
Additionally, trying to ignore the error leads to excessive memory consumption as the SDK continues serializing the loop. Adjusting the MaxDepth property on JsonSerializerSettings to different values, such as 5, does not seem to have any effect.
Reproduce steps
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddOData(options =>
options.EnableQueryFeatures(50).AddRouteComponents("odata", new MdsPeopleDataModel().GetEntityDataModel()))
.AddODataNewtonsoftJson();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
public class MdsPeopleDataModel
{
public IEdmModel GetEntityDataModel()
{
ODataConventionModelBuilder builder = new();
builder.ComplexType<User>();
builder.EntitySet<Employee>("Employees");
return builder.GetEdmModel();
}
}
EmployeesController.cs
[Route("[controller]")]
public class EmployeesController : ODataController
{
[EnableQuery]
public IEnumerable<Employee> Get()
{
string endpoint = "https://localhost:8081";
string authKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
CosmosClient client = new CosmosClient(endpoint, authKey);
Container? container = client.GetContainer("DummyDb", "DummyContainer");
IOrderedQueryable<Employee>? query = container.GetItemLinqQueryable<Employee>(true);
return query;
}
}
Data Model
public class Employee
{
public string id { get; set; }
public User User { get; set; }
public string _rid { get; set; }
public string _self { get; set; }
public string _etag { get; set; }
public string _attachments { get; set; }
public int _ts { get; set; }
}
public class User
{
public string id { get; set; }
public string Name { get; set; }
public ICollection<Email> Emails { get; set; }
}
public class Email
{
public string EmailAdress { get; set; }
public string Type { get; set; }
}
EDM (CSDL) Model
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<ComplexType Name="User">
<Property Name="id" Type="Edm.String" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Emails" Type="Collection(Default.Email)" />
</ComplexType>
<EntityType Name="Employee">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.String" Nullable="false" />
<Property Name="User" Type="Default.User" />
<Property Name="_rid" Type="Edm.String" />
<Property Name="_self" Type="Edm.String" />
<Property Name="_etag" Type="Edm.String" />
<Property Name="_attachments" Type="Edm.String" />
<Property Name="_ts" Type="Edm.Int32" Nullable="false" />
</EntityType>
<ComplexType Name="Email">
<Property Name="EmailAdress" Type="Edm.String" />
<Property Name="Type" Type="Edm.String" />
</ComplexType>
<EntityContainer Name="Container">
<EntitySet Name="Employees" EntityType="Default.Employee" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Entity
{
"id": "1",
"User": {
"id": "1",
"Name": "John",
"Emails": [
{
"EmailAdress": "john@1.com",
"Type": "A"
}
]
},
"_rid": "jqYgAN5HGIMBAAAAAAAAAA==",
"_self": "dbs/jqYgAA==/colls/jqYgAN5HGIM=/docs/jqYgAN5HGIMBAAAAAAAAAA==/",
"_etag": "\"00000000-0000-0000-eac5-74b845c101d9\"",
"_attachments": "attachments/",
"_ts": 1695106177
}
Request/Response
https://localhost:5001/odata/employees?select=user
Expected behavior
The desired outcome is to permit the projection of solely the chosen User or its internal properties, all while avoiding the JSON serialization problem, or alternatively, enabling the option to exclude the internal properties from the EdmModel.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.