【问题标题】:Thread safety with Entity Framework实体框架的线程安全
【发布时间】:2016-02-19 19:57:18
【问题描述】:

(我不知道问题是否真的与线程安全有关,所以我会酌情编辑标题。)

.NET 4.6,MVC 5。我在我管理的站点上运行一个任务,该任务获取多个 CSV 文件并遍历它们,根据需要操作内部数据(这些文件是只读的)。此任务每 20 分钟运行一次。

任务完美运行了几天,但昨天它停止处理数据。它在预定的时间运行,但每个 CSV 文件的数据处理单独失败并出现错误The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects. 重新启动应用程序解决了几个小时的问题,然后又开始出错。

我不知道这可能与什么有关,但我的直觉反应是线程安全。我并不完全熟悉 .NET/MVC 中线程的处理方式,但粗略的搜索表明 EF 不是线程安全的。是否有可能我的任务正试图在自身之上运行而ObjectContext 正因此而遇到麻烦?这似乎很奇怪,因为在我看来,任务的单独实例应该有自己的上下文。也许更有可能是服务如何执行数据库操作。

由于其性质,我无法重现该问题;调试时在本地重现它的所有尝试都显示任务按预期运行。

什么可能导致这些错误,我该如何预防?

我已经包含了第一个 CSV 文件导入的代码;每个部分的异常都被单独捕获。对于背景,User 有一个名为 PlannerCodes 的属性 ICollection<PlannerCode>。这个项目是基于 nopCommerce 的,但是这个任务是完全自定义的,所以我相信它不仅适用于 nopCommerce,而且我不会这样标记它。

堆栈跟踪:

System.InvalidOperationException: The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.ValidateContextsAreCompatible(RelatedEnd targetRelatedEnd)
at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.Add(IEntityWrapper wrappedTarget, Boolean applyConstraints, Boolean addRelationshipAsUnchanged, Boolean relationshipAlreadyExists, Boolean allowModifyingOtherEndOfRelationship, Boolean forceForeignKeyChanges)
at System.Data.Entity.Core.Objects.ObjectStateManager.PerformAdd(IEntityWrapper wrappedOwner, RelatedEnd relatedEnd, IEntityWrapper entityToAdd, Boolean isForeignKeyChange)
at System.Data.Entity.Core.Objects.ObjectStateManager.PerformAdd(IList`1 entries)
at System.Data.Entity.Core.Objects.ObjectStateManager.DetectChanges()
at System.Data.Entity.Core.Objects.ObjectContext.DetectChanges()
at System.Data.Entity.Internal.InternalContext.DetectChanges(Boolean force)
at System.Data.Entity.Internal.InternalContext.GetStateEntries(Func`2 predicate)
at System.Data.Entity.Internal.InternalContext.GetStateEntries()
at System.Data.Entity.Infrastructure.DbChangeTracker.Entries()
at System.Data.Entity.DbContext.GetValidationErrors()
at System.Data.Entity.Internal.InternalContext.SaveChanges()
at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
at System.Data.Entity.DbContext.SaveChanges()
at Nop.Data.EfRepository`1.Update(T entity) in C:\Users\username\Documents\projectname\Libraries\Nop.Data\EfRepository.cs:line 120
at Nop.Services.Users.UserService.UpdateUser(User user) in C:\Users\username\Documents\projectname\Libraries\Nop.Services\Users\UserService.cs:line 477
at Nop.Services.WorkItems.ImportTask.Execute() in C:\Users\username\Documents\projectname\Libraries\Nop.Services\WorkItems\ImportTask.cs:line 165

ImportTask代码:

if (File.Exists(plannerFile))
{
    try
    {
        // planners corresponds to the CSV file
        // foreach is the correct way of iterating through the lines
        foreach (var p in planners)
        {
            var user = _userService.GetUserByEmail(p.Email);
            var pcode = _codeService.GetPCodeByString(p.Code);

            // check if pcode already exists. if it doesn't, insert it.
            if (pcode == null)
            {
                pcode = new PlannerCode { P = p.Code };
                _codeService.InsertPCode(pcode);
            }

            // if no user found or the user is already associated with the pcode, move on
            if (user == null || user.PlannerCodes.Contains(pcode))
                continue;

            // add the pcode to the user's PlannerCodes
            user.PlannerCodes.Add(pcode);

            // update the user to save changes to PlannerCodes
            _userService.UpdateUser(user);
        }
    }
    catch (Exception ex)
    {
        // log exception info, ex.ToString()
    }
}

UserService 在下面。 CodeService 基本相同,只是相关的存储库类型发生了变化。

readonly IRepository<User> _userRepository;

public UserService(IRepository<User> userRepository)
{
    _userRepository = userRepository;
}

public virtual User GetUserByEmail(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return null;

    return _userRepository.Table.FirstOrDefault(u => u.Email == email);
}

public virtual void UpdateUser(User user)
{
    if (user == null)
        throw new ArgumentNullException(nameof(user));

    _userRepository.Update(user);
}

EfRepository:

public virtual void Update(T entity)
{
    // exceptions are caught but snipped from this example
    _context.SaveChanges();
}

使用以下代码将服务注入ImportTask

IUserService _userService = EngineContext.Current.Resolve<IUserService>();

【问题讨论】:

  • IRepository 和 IRepository 使用不同的上下文?
  • @ArturoMenchaca 我现在正在阅读 Autofac 文档,老实说,我不太确定这一切是如何工作的。我相信相关代码在DependencyRegistrar.cs(来源here)中,它将DbContext设置为InstancePerLifetimeScope,但我试图更好地理解。
  • 您必须对相同的请求使用相同的上下文。如果你需要在任何东西之间共享实体,你应该使用 UnitOfWork

标签: c# asp.net-mvc entity-framework


【解决方案1】:

当您关联在DbContext 的不同实例中创建的实体时会发生此类错误。

如果 UserServiceCodeService 的存储库具有不同的上下文,则此错误将在某些时候发生。

正如您在 cmets 中所说,如果您使用 InstancePerLifetimeScope,则每个服务的上下文可能不同,因为存储库是在不同的范围内创建的。

您应该使用InstancePerRequest 来确保在整个请求的执行过程中上下文是相同的。

【讨论】:

  • 这可能是 nopCommerce 特有的问题,但我不确定。有没有办法我可以在任务本身内声明一个上下文,并使任务完成的任何事情都使用该上下文?我宁愿不编辑框架的核心文件;我可能会破坏一切。
  • @Vaindil: UserService 和 CodeService 被注入到 ImportTask 中?
  • 正确。我将代码添加到问题的底部。我不知道这是否特定于 nopCommerce。
  • @Vaindil:我从未与 nopCommerce 合作过,但我在 github 上看到的是,是的。如果您无法编辑 EfRepository ,也许黑客可以解决任务的上下文,因此所有嵌套范围都将使用相同的上下文。另一种尝试可能是将自己的上下文注册为 InstancePerRequest,也许这优先于 nopEcommerce 默认配置
  • @Vaindil:如果您可以创建自己的存储库或从 EfRepositoy 继承,则可以创建一个接收上下文作为构造函数参数(要注入)的类 UnitOfWork,并将该类注册为 InstancePerRequest,所以只有创建一个上下文。创建的存储库类可能会收到 UnitOfWork 而不是上下文
猜你喜欢
  • 2011-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多