OData / OData/AspNetCoreOData

JSON Serialization Issue with OData and Self-Referencing Complex Types

Open
#1,056 11 comments 0 reactions 1 assignee View on GitHub

@ElizabethOkerio is already working on this.

Since Sep 19, 2023.

bug
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.

2023-09-19 09_05_15-Microsoft Azure Cosmos – ExpressionToSQL

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.