OptimisticConcurrencyException Does Not Work in Entity Framework In Certain Situations

前端 未结 4 1432
小鲜肉
小鲜肉 2020-12-24 02:59

UPDATE (2010-12-21): Completely rewrote this question based on tests that I\'ve been doing. Also, this used to be a POCO specific question, but it turns out that my

相关标签:
4条回答
  • 2020-12-24 03:04

    Explanation

    The reason why you aren't getting the expected OptimisticConcurrencyException on your second code example is due to the manner EF checks concurrency:

    When you retrieve entities by querying your db, EF remembers the value of all with ConcurrencyMode.Fixed marked properties by the time of querying as the original, unmodified values.

    Then you change some properties (including the Fixed marked ones) and call SaveChanges() on your DataContext.

    EF checks for concurrent updates by comparing the current values of all Fixed marked db columns with the original, unmodified values of the Fixed marked properties. The key point here is that EF treats the update of you timestamp property as a normal data property update. The behavior you see is by design.

    Solution/Workaround

    To workaround you have the following options:

    1. Use your first approach: Don't requery the db for your entity but Attach the recreated entity to your context.

    2. Fake your timestamp value to be the current db value, so that the EF concurrency check uses your supplied value like shown below (see also this answer on a similar question):

      var currentPerson = _context.People.Where(x => x.Id == person.Id).First();
      currentPerson.VerColm = person.VerColm; // set timestamp value
      var ose = _context.ObjectStateManager.GetObjectStateEntry(currentPerson);
      ose.AcceptChanges();       // pretend object is unchanged
      currentPerson.Name = person.Name; // assign other data properties
      _context.SaveChanges();
      
    3. You can check for concurrency yourself by comparing your timestamp value to the requeried timestamp value:

      var currentPerson = _context.People.Where(x => x.Id == person.Id).First();
      if (currentPerson.VerColm != person.VerColm)
      {
          throw new OptimisticConcurrencyException();
      }
      currentPerson.Name = person.Name; // assign other data properties
      _context.SaveChanges();
      
    0 讨论(0)
  • 2020-12-24 03:13

    If it's EF Code first, then use code similar to below code. This will change the original TimeStamp loaded from db to the one from UI and will ensure OptimisticConcurrencyEception occurs.

    db.Entry(request).OriginalValues["Timestamp"] = TimeStamp;
    
    0 讨论(0)
  • 2020-12-24 03:18

    Here is another approach that is a bit more generic and fits in the data layer:

    // if any timestamps have changed, throw concurrency exception
    var changed = this.ChangeTracker.Entries<>()
        .Any(x => !x.CurrentValues.GetValue<byte[]>("Timestamp").SequenceEqual(
            x.OriginalValues.GetValue<byte[]>("Timestamp")));
    if (changed) throw new OptimisticConcurrencyException();
    this.SaveChanges();
    

    It just checks to see if the TimeStamp has changed and throws concurrency exception.

    0 讨论(0)
  • 2020-12-24 03:24

    I have modified @JarrettV solution to work with Entity Framework Core. Right now it is iterating through all modified entries in context and looking for any mismatch in property marked as concurrency token. Works for TimeStamp (RowVersion) as well:

    private void ThrowIfInvalidConcurrencyToken()
    {
        foreach (var entry in _context.ChangeTracker.Entries())
        {
            if (entry.State == EntityState.Unchanged) continue;
    
            foreach (var entryProperty in entry.Properties)
            {
                if (!entryProperty.IsModified || !entryProperty.Metadata.IsConcurrencyToken) continue;
    
                if (entryProperty.OriginalValue != entryProperty.CurrentValue)
                {                    
                    throw new DbUpdateConcurrencyException(
                        $"Entity {entry.Metadata.Name} has been modified by another process",
                        new List<IUpdateEntry>()
                        {
                            entry.GetInfrastructure()
                        });
                }
            }
        }
    }
    

    And we need only to invoke this method before we save changes in EF context:

    public async Task SaveChangesAsync(CancellationToken cancellationToken)
    {
        ThrowIfInvalidConcurrencyToken();
        await _context.SaveChangesAsync(cancellationToken);
    }
    
    0 讨论(0)
提交回复
热议问题