使用EF Core Include方法查询时出现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类似,此处略。
请问该场景中问题出在哪里?
问题原因及解决方案
核心原因
集合类型差异导致EF处理逻辑不同
Film聚合根公开的是IReadOnlyList<CountryItem>,而正常运行的Order用的是IReadOnlyCollection<OrderItem>。EF Core对IReadOnlyList的映射会尝试直接操作只读集合实例(AsReadOnly()返回的ReadOnlyCollection<T>无法修改),从而抛出只读异常;而IReadOnlyCollection会触发EF自动查找对应的私有可写集合(遵循_<属性名>的命名约定)。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

