EF6+JSON序列化报错:访问已释放DbContext获取未请求关联数据
遇到这个问题挺典型的——EF6默认的延迟加载在DbContext释放后,序列化器访问导航属性就会报错,加上多对多的Include容易踩坑,给你几个不用DTO就能解决的方案:
先解决CS0311错误:正确的Include姿势
首先得确认你的多对多关系是隐式还是显式的,这直接影响Include的写法:
情况1:隐式多对多(无CountryCars实体)
如果你的DbContext里没有DbSet<CountryCars>,EF自动帮你维护中间表,那Country类应该有这样的导航属性:
public class Country { public int Id { get; set; } public string Name { get; set; } // 必须是virtual触发延迟加载,且类型为ICollection<Car> public virtual ICollection<Car> Cars { get; set; } = new List<Car>(); }
Car类对应:
public class Car { public int Id { get; set; } public string Model { get; set; } public virtual ICollection<Country> Countries { get; set; } = new List<Country>(); }
并且在DbContext的OnModelCreating里配置多对多:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Country>() .HasMany(c => c.Cars) .WithMany(c => c.Countries) .Map(cs => { cs.MapLeftKey("CountryId"); cs.MapRightKey("CarId"); cs.ToTable("CountryCars"); }); }
这种情况下直接用Include(c => c.Cars)就不会报CS0311:
var countries = db.Countries.Include(c => c.Cars).ToList();
情况2:显式多对多(有CountryCars实体)
如果你的DbContext里有DbSet<CountryCars>(比如中间表有额外字段,必须显式建模),那Country的导航属性应该是ICollection<CountryCars>,而不是直接ICollection<Car>。这时候需要先Include中间表,EF6没有ThenInclude,要换成嵌套Select的写法:
var countries = db.Countries .Include(c => c.CountryCars.Select(cc => cc.Car)) .ToList();
解决ObjectDisposedException的核心方案
不管你要不要返回关联的Cars数据,核心都是避免在DbContext释放后触发延迟加载,下面是几种思路:
方案1:临时/全局关闭延迟加载
如果不需要返回Cars数据,最简单的方式就是在查询时临时关闭延迟加载,这样序列化器不会去访问未加载的导航属性:
using (var db = new YourDbContext()) { // 临时关闭当前上下文的延迟加载 db.Configuration.LazyLoadingEnabled = false; var countries = db.Countries.ToList(); return countries; }
如果整个项目都不需要延迟加载,也可以在DbContext的构造函数里全局关闭:
public YourDbContext() : base("YourConnectionString") { this.Configuration.LazyLoadingEnabled = false; this.Configuration.ProxyCreationEnabled = false; // 可选,关闭代理类,进一步避免延迟加载 }
方案2:预先加载所有需要的关联数据
如果确实需要返回Cars数据,就必须在DbContext释放前把关联数据加载到内存——也就是正确使用上面说的Include/嵌套Select,确保查询时一次性拉取所有需要的数据,这样序列化时就不需要再访问DbContext了。
方案3:手动加载导航属性(适合分批/条件加载)
如果已经查询了Country,还想加载特定的关联Cars,可以用Load()方法在DbContext存活时手动加载:
using (var db = new YourDbContext()) { var countries = db.Countries.ToList(); foreach (var country in countries) { // 手动加载当前Country的Cars db.Entry(country).Collection(c => c.Cars).Load(); } return countries; }
方案4:配置Json序列化器忽略未加载的属性
如果不想改查询逻辑,也可以通过配置Json.NET(Web API默认序列化器)来忽略未加载的导航属性,避免触发延迟加载。在WebApiConfig.cs的Register方法里添加:
public static void Register(HttpConfiguration config) { // 处理循环引用(多对多容易出现) config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // 忽略未加载的EF导航属性 config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DefaultContractResolver { ResolveContract = type => { var contract = base.ResolveContract(type); // 只处理EF实体类型 if (typeof(System.Data.Entity.Core.Objects.DataClasses.IEntityWithRelationships).IsAssignableFrom(type)) { foreach (var property in contract.Properties.Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>))) { property.ShouldSerialize = instance => { var entity = instance as System.Data.Entity.Core.Objects.DataClasses.IEntityWithRelationships; if (entity == null) return true; // 只序列化已经加载的导航属性 var relatedEnd = entity.RelationshipManager.GetRelatedEnd(property.PropertyName); return relatedEnd.IsLoaded; }; } } return contract; } }; }
这样序列化器只会序列化已经加载的关联数据,不会去触碰未加载的属性,自然就不会触发DbContext已释放的错误。
内容的提问来源于stack exchange,提问作者beggarboy

