0

(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>();
vaindil
  • 7,536
  • 21
  • 68
  • 127
  • Related: http://stackoverflow.com/questions/15274539/the-relationship-between-the-two-objects-cannot-be-defined-because-they-are-atta – Arturo Menchaca Feb 19 '16 at 20:01
  • IRepository and IRepository use different contexts? – Arturo Menchaca Feb 19 '16 at 20:04
  • @ArturoMenchaca I'm reading through the Autofac docs right now, I'm honestly not quite sure how that all works. I believe the relevant code is in `DependencyRegistrar.cs` (source [here](https://github.com/nopSolutions/nopCommerce/blob/develop/src/Presentation/Nop.Web.Framework/DependencyRegistrar.cs)), which sets the DbContext to `InstancePerLifetimeScope`, but I'm trying to understand better. – vaindil Feb 19 '16 at 20:15
  • You must use the same context for the same request. If you need to share entities between anything you should use an UnitOfWork – Fals Feb 19 '16 at 20:21

1 Answers1

1

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.

Arturo Menchaca
  • 15,783
  • 1
  • 29
  • 53
  • This may be a question specific to nopCommerce, but I'm not sure. Is there a way I can declare a context inside the task itself and make anything done by the task use that context? I would rather not edit the core files of the framework; I would probably break everything. – vaindil Feb 19 '16 at 20:31
  • @Vaindil: UserService and CodeService are injected into ImportTask? – Arturo Menchaca Feb 19 '16 at 20:50
  • Correct. I added the code to the bottom of the question. I don't know if that's nopCommerce-specific. – vaindil Feb 19 '16 at 20:55
  • @Vaindil: I've never worked with nopCommerce but what I saw at github yes it is. If you cant edit EfRepository<> maybe a hacks could be resolve a context on the task, so all nested scope will use the same context. Another try might be registering yourself the context as InstancePerRequest, maybe this takes precedence to nopEcommerce defaults config – Arturo Menchaca Feb 19 '16 at 21:18
  • @Vaindil: If you can creates your own repository or inherit from EfRepositoy, you can create a class UnitOfWork that receives a context as constructor argument (to be injected) and register this class as InstancePerRequest, so only one context is created. the repository class created may receive an UnitOfWork instead of a context – Arturo Menchaca Feb 19 '16 at 21:27
  • also be sure to refresh the context from time to time for performance reasons – user853710 Feb 20 '16 at 00:19