简体   繁体   中英

Thread safety with Entity Framework

(I don't know if the problem is actually related to thread safety, so I will edit the title as appropriate.)

.NET 4.6, MVC 5. I run a task on a site I manage that grabs several CSV files and iterates through them, manipulating internal data as necessary (the files are read-only). This task runs every 20 minutes.

The task ran perfectly for several days, but yesterday it stopped processing data. It runs at the scheduled times, but the data processing for each CSV file fails individually with the error The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects. Restarting the application fixed the problem for several hours, then it began to error again.

I don't know what this could be related to, but my gut reaction is thread safety. I'm not completely familiar with how threads are handled in .NET/MVC, but a cursory search shows that EF is not thread-safe. Is it possible that my task is trying to run on top of itself and the ObjectContext is having trouble because of that? This seems bizarre, because it seems to me that a separate instance of the task should have its own context. Perhaps more likely is something with how the services perform the database operations.

I can't reproduce the problem because of its nature; all attempts to reproduce it locally while debugging show the task running as expected.

What could be causing these errors, and how can I prevent them?

I have included the code for the first CSV file import; exceptions on each part are caught separately. For background, User has a property ICollection<PlannerCode> called PlannerCodes . This project is based on nopCommerce, but this task is fully custom so I believe it's applicable to more than nopCommerce and I'm not tagging it that way.

Stack trace:

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 code:

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 is below. CodeService is essentially the same but with the relevant repository types changed.

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();
}

Services are injected into ImportTask with this code:

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

This type of error occurs when you relate entities created in different instances of DbContext .

If repositories of UserService and CodeService have different contexts this error will occur at some point.

As you says in comments if you are using InstancePerLifetimeScope probably contexts of each service are different because the repositories are created in different scopes.

You should use InstancePerRequest to ensure that the context is the same throughout the execution of the entire request.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM