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

使用EF Core Include方法查询时出现Collection is read-only异常

EF Core查询聚合根时"Collection is read-only"异常排查

问题描述

使用EF Core从SQL Server查询Film聚合根时抛出异常:

System.NotSupportedException: Collection is read-only

查询代码:

var film = await dbContext.Films
     .Include(f => f.CountryItems)
     .FirstOrDefaultAsync(f => f.Id == FilmId.Of(query.id), cancellationToken)

Film聚合根定义

public class Film : Aggregate<FilmId>
{
    private readonly List<CountryItem> _filmCountries = [];
    public IReadOnlyList<CountryItem> CountryItems => _filmCountries.AsReadOnly();
    ...
}

CountryItem实体定义

public class CountryItem : Entity<FilmCountryId>
{
    protected CountryItem() { }

    internal CountryItem(CountryName name, FilmId filmId)
    {
        Name = name;
        FilmId = filmId;
    }

    public CountryName Name { get; private set; } = default!;
    public FilmId FilmId { get; private set; } = default!;
}

Film的EF配置类

public class FilmsConfiguration : IEntityTypeConfiguration<Film>
{
    public void Configure(EntityTypeBuilder<Film> builder)
    {
        builder.HasKey(f => f.Id);
        builder.Property(f => f.Id)
               .HasConversion(filmid => filmid.Value, f => FilmId.Of(f))
                .ValueGeneratedOnAdd()
                .UseIdentityColumn();

        builder.HasMany(f => f.CountryItems)
                .WithOne()
                .HasForeignKey(c => c.FilmId);
    }
}

CountryItem的EF配置类

public class CountryItemsConfiguration : IEntityTypeConfiguration<CountryItem>
{
    public void Configure(EntityTypeBuilder<CountryItem> builder)
    {
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Id)
            .HasConversion(c => c.Value, id => FilmCountryId.Of(id))
            .ValueGeneratedOnAdd()
            .UseIdentityColumn();

        builder.Property(c => c.Name).HasMaxLength(50).IsRequired()
            .HasConversion(c => c.Value, name => CountryName.Of(name));

        builder.Property(c => c.FilmId)
            .HasConversion(c => c.Value, c => FilmId.Of(c))
            .IsRequired();
    }
}

对比正常运行的类似实现

另一个项目中,Order聚合根的类似实现可正常运行:

查询代码:

var orders = await dbContext.Orders
                .Include(o => o.OrderItems)
                .FirstOrDefaultAsync(o=>o.Id == OrderId.Of(new Guid("7e761dbd-1aaa-4640-8db6-d12a883e4080")),cancellationToken);

Order聚合根定义:

public class Order : Aggregate<OrderId>
{
    private readonly List<OrderItem> _orderItems = new();
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
...
}

OrderItem实体及EF配置类结构与CountryItem类似,此处略。

请问该场景中问题出在哪里?


问题原因及解决方案

核心原因

  1. 集合类型差异导致EF处理逻辑不同
    Film聚合根公开的是IReadOnlyList<CountryItem>,而正常运行的Order用的是IReadOnlyCollection<OrderItem>。EF Core对IReadOnlyList的映射会尝试直接操作只读集合实例(AsReadOnly()返回的ReadOnlyCollection<T>无法修改),从而抛出只读异常;而IReadOnlyCollection会触发EF自动查找对应的私有可写集合(遵循_<属性名>的命名约定)。

  2. EF未明确关联私有可写集合
    即使命名符合约定,部分EF Core版本对IReadOnlyList的自动识别支持不佳,导致EF无法定位到底层的_filmCountries私有集合,只能尝试修改公开的只读属性。

解决方案

方案一:修改公开集合类型为IReadOnlyCollection

将Film中的CountryItems类型改为IReadOnlyCollection<CountryItem>,与Order保持一致:

public class Film : Aggregate<FilmId>
{
    private readonly List<CountryItem> _filmCountries = [];
    public IReadOnlyCollection<CountryItem> CountryItems => _filmCountries.AsReadOnly();
    ...
}

此方案利用EF的命名约定自动关联私有集合,无需修改配置。

方案二:显式配置EF使用私有字段

在Film的配置类中,明确指定关联集合的底层私有字段:

public class FilmsConfiguration : IEntityTypeConfiguration<Film>
{
    public void Configure(EntityTypeBuilder<Film> builder)
    {
        // 其他配置...

        builder.HasMany(f => f.CountryItems)
                .WithOne()
                .HasForeignKey(c => c.FilmId);
        
        // 显式指定EF使用私有字段作为集合存储
        builder.Navigation(f => f.CountryItems)
               .HasField("_filmCountries")
               .UsePropertyAccessMode(PropertyAccessMode.Field);
    }
}

此方案无需修改实体类,直接通过配置告诉EF访问私有可写集合。

方案三:通过元数据指定集合字段

另一种简洁的配置方式:

builder.HasMany(f => f.CountryItems)
       .WithOne()
       .HasForeignKey(c => c.FilmId)
       .Metadata.PrincipalToDependent.SetField("_filmCountries");

直接通过关联元数据设置底层字段,效果与方案二一致。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 12:22:34