#4649 Feature/4649 thread safe broken rules#4819
Conversation
…lock and also for the ToThreadsafeList method to use the lock as well and return a copy of the broken rules collection.
|
Can we use something a little lighter than a Can the underlying collection be one of the concurrent types now available in .NET? Merging into main is fine as long as this isn't a breaking change. We're currently building toward 10.1.0, which is a feature+fix release, but can't include breaking changes. |
There was a problem hiding this comment.
Pull request overview
This PR addresses issue #4649 by adding thread-safety mechanisms to BrokenRulesCollection to prevent InvalidOperationException when async business rules complete during enumeration operations. The solution introduces a new ToThreadsafeList() method that returns a snapshot of the collection protected by the existing _syncRoot lock, and adds locking to the public AddRange() method.
Changes:
- Added
ToThreadsafeList()method toBrokenRulesCollectionthat creates a thread-safe snapshot - Added lock protection to the
AddRange()method - Added test to verify thread safety of the new method
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| Source/Csla/Rules/BrokenRulesCollection.cs | Implements ToThreadsafeList() method and adds locking to AddRange() to prevent collection modification exceptions |
| Source/tests/Csla.test/BizRules/BusinessRuleTests.cs | Adds test class and test method to validate thread-safe enumeration with concurrent modifications |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| lock (_syncRoot) | ||
| { | ||
| foreach (var item in list) | ||
| Add(item); | ||
| } |
There was a problem hiding this comment.
The AddRange method now acquires the _syncRoot lock and then calls Add() in a loop. However, the Add() method is declared with the 'new' keyword (line 144) rather than 'override', which means callers using the base class reference type could bypass this method and the associated counter updates. Additionally, since Add() is not protected by any lock internally, calling it within the lock doesn't actually provide thread safety for the Add operation itself - it only ensures AddRange iterations aren't interrupted. Consider reviewing whether Add() should also be protected by the lock or if there are scenarios where the base class Add() could be called directly.
There was a problem hiding this comment.
This limitation was explained in the notes for the checkin
| lock (_syncRoot) | ||
| { | ||
| return this.ToList(); | ||
| } |
There was a problem hiding this comment.
The method creates a complete copy of the collection using ToList() while holding the lock. For collections with many broken rules, this could hold the lock for a non-trivial duration, potentially blocking async rule completion callbacks. Consider whether this is acceptable for the use case, or if a more efficient snapshot mechanism (such as using an immutable collection internally) would be better for performance.
There was a problem hiding this comment.
This is a bit outdated but I find it unlikely a broken rules collection would ever have enough items in here to make this an issue.
@rockfordlhotka,
That's not to say I can't use SemephoreSlim but it starts begging the question if that means all the other lock statements should also be examined for the same reason. The BrokenRulesCollection ultimately inherits from ObservableCollection for what I assume to be binding reasons after going through an inheritance chain running through several CSLA collection classes. That is of course not thread safe. If I changed what BrokenRulesCollection was inheriting from I assume it would be a braking change and also would need a lot of other code like implementing INotifyCollectionChanged and INotifyPropertyChanged. All possible solutions as well. I'm just not sure what would be given up if it no longer inherited from ReadOnlyObservableBindingList. It looks like mobile formatter code is in there and that is just in the first CSLA class down the inheritance chain. I do have some other ways to do the test that I think may be better and guarantee to cause the error. |
…List to return an ObservableCollection.
That all makes sense to me, thank you. Let's stick with |
#4649
@rockfordlhotka This PR is not ready to merge but I think it bears some discussion. The following points should be considered:
Right now, this PR is just for discussion on approach and also those Items I listed.