You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core操作CosmosDB时嵌套JSON无法保存的问题排查

问题描述

使用技术栈:
-.NET Core 6
-EF Core 6
-CosmosDB

定义的实体类(MenuItem支持嵌套结构):

public class BaseModel
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }
}

public class Menu : BaseModel
{
    public string Discriminator = nameof(Menu);
    [MaxLength(100)]
    public string Name { get; set; }
    [MaxLength(100)]
    public string Slug { get; set; }

    public ICollection<MenuItem> Items { get; set; } = new List<MenuItem>();
}

// 尝试过继承和不继承BaseModel
public class MenuItem : BaseModel
{
    [MaxLength(100)]
    public string Name { get; set; }
    [MaxLength(100)]
    public string Slug { get; set; }
    [MaxLength(1000)]
    public string Url { get; set; }        

    // 注意此嵌套属性
    public ICollection<MenuItem> Items { get; set; } = new List<MenuItem>();
}

CosmosContext定义:

public class CosmosContext : DbContext
{
    public CosmosContext(DbContextOptions<CosmosContext> options) : base(options) { }
    public virtual DbSet<Menu> Menus { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<Menu>(b =>
        {
            b.HasKey(o => o.Id);
            b.HasPartitionKey(o => o.Id);
            b.HasDiscriminator<string>(nameof(Menu.Discriminator));
        });
    }  
}  

保存数据的代码:

// 添加包含嵌套MenuItem的新菜单
var m = new Menu()
{
    Name = "Example Menu",
    Slug = "example-menu",
    Items = new List<MenuItem>()
    {
        new MenuItem() // 此层级可保存到CosmosDB
        {
            Name = "Menu 1",
            Slug = "menu-1",
            Items = new List<MenuItem>()
            {
                new MenuItem() // 此层级未出现在CosmosDB中
                {
                    Name = "Menu 1a",
                    Slug = "menu-1a"
                }
            }
        }
    }
};

context.Menus.Add(m);
await context.SaveChangesAsync();

保存后CosmosDB中的文档:

{
    "Id": "24b9e5fd-90c9-47bb-bec7-08daac957c98",
    "Discriminator": "Menu",
    "Name": "Example Menu",
    "Slug": "example-menu",
    "id": "Menu|24b9e5fd-90c9-47bb-bec7-08daac957c98",
    "Items": [
        {
            "Id": "2e8003ec-5c2e-463f-add7-08daac957ca0",
            "MenuId": "24b9e5fd-90c9-47bb-bec7-08daac957c98",
            "Name": "Menu 1",
            "Slug": "menu-1",
            "Url": null
        }
    ],
    "_rid": "uYkaAKC8Y1EBAAAAAAAAAA==",
    "_self": "dbs/uYkaAA==/colls/uYkaAKC8Y1E=/docs/uYkaAKC8Y1EBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-de7e-5a4c14e301d8\"",
    "_attachments": "attachments/",
    "_ts": 1665608726
}

发现文档中缺少"Menu 1a"对应的嵌套Items属性,请问:

  1. 是否存在明显的错误操作?
  2. EF Core/CosmosDB是否不支持这种嵌套结构?

问题解答

1. 存在的明显错误

EF Core默认会把继承自BaseModel的MenuItem识别为独立实体,而非嵌套的复杂类型。因为MenuItem继承了带有[Key]属性的基类,EF Core会将其视为需要单独跟踪的实体对象,而非嵌入父文档的子结构。同时你未在OnModelCreating中显式配置MenuItem的嵌套映射规则,导致EF Core仅处理了第一层关联,深层嵌套关系被直接忽略。

2. EF Core/CosmosDB支持这种嵌套结构,但需正确配置

EF Core的CosmosDB提供器完全支持多层嵌套的复杂类型结构,但需要满足两个核心要求:

  • 嵌套类型不能被EF Core识别为独立实体(即不能拥有主键,或需显式配置为复杂类型)
  • 必须显式配置嵌套属性的映射规则

修复方案

方案一:将MenuItem改为无主键的复杂类型

移除MenuItem对BaseModel的继承,或删除其主键属性:

public class MenuItem
{
    [MaxLength(100)]
    public string Name { get; set; }
    [MaxLength(100)]
    public string Slug { get; set; }
    [MaxLength(1000)]
    public string Url { get; set; }        

    public ICollection<MenuItem> Items { get; set; } = new List<MenuItem>();
}

方案二:显式配置MenuItem为复杂类型(保留继承关系)

在OnModelCreating中显式标记MenuItem为嵌套复杂类型,并配置多层嵌套规则:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<Menu>(b =>
    {
        b.HasKey(o => o.Id);
        b.HasPartitionKey(o => o.Id);
        b.HasDiscriminator<string>(nameof(Menu.Discriminator));
        // 配置第一层Items为嵌套集合
        b.OwnsMany(m => m.Items, itemBuilder =>
        {
            // 配置MenuItem的嵌套Items属性
            itemBuilder.OwnsMany(i => i.Items);
            // 若需保留Id,将其配置为普通自动生成属性而非主键
            itemBuilder.Property(i => i.Id).ValueGeneratedOnAdd();
        });
    });
    // 禁止EF Core将MenuItem视为独立实体
    builder.Ignore<MenuItem>();
}

修复后效果

保存后CosmosDB文档会包含完整的多层嵌套结构:

{
    "Id": "xxx",
    "Discriminator": "Menu",
    "Name": "Example Menu",
    "Slug": "example-menu",
    "Items": [
        {
            "Id": "xxx",
            "Name": "Menu 1",
            "Slug": "menu-1",
            "Url": null,
            "Items": [
                {
                    "Id": "xxx",
                    "Name": "Menu 1a",
                    "Slug": "menu-1a",
                    "Url": null
                }
            ]
        }
    ],
    // 其他系统字段...
}

内容的提问来源于stack exchange,提问作者redapollos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 15:05:16