Fix code quality: cache invalidation, resource disposal, null safety, and test assertions#218
Fix code quality: cache invalidation, resource disposal, null safety, and test assertions#218
Conversation
- Fix cache invalidation inconsistency: use InvalidateCacheAsync instead of Cache.RemoveAllAsync so subclass overrides are respected - Add DocumentsChanged handler check to UpdateByQuery fast-path to prevent silently missed change events when listeners are registered - Fix PatchDocument.Load(Stream) StreamReader leak with using/leaveOpen - Add null guard to Operation.Build to prevent NullReferenceException - Preserve inner exception in PatchDocumentConverter error handling - Add explicit type validation in PatchDocument.Load(JArray) - Split Assert.NotNull(x?.Id) into separate assertions across all test files (136 instances) to prevent false-passing null object tests Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
This PR focuses on improving correctness and code quality across the Elasticsearch repository and JSON patch components, while also tightening test assertions to avoid false positives.
Changes:
- Fix cache invalidation and patch-by-query behavior in
ElasticRepositoryBase.PatchAllAsync. - Improve JSON Patch parsing/validation and exception preservation (
PatchDocument*,Operation). - Update many tests to split null-conditional assertions into explicit null checks.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Foundatio.Repositories.Elasticsearch.Tests/VersionedTests.cs | Split Assert.NotNull(x?.Id) into explicit null assertions. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/RepositoryTests.cs | Same test assertion tightening across multiple tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/ReindexTests.cs | Same test assertion tightening for reindex scenarios. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/ReadOnlyRepositoryTests.cs | Same test assertion tightening for read-only repo tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/QueryableRepositoryTests.cs | Same test assertion tightening for queryable repo tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/QueryTests.cs | Same test assertion tightening for query behavior tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/ParentChildTests.cs | Same test assertion tightening for parent/child tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/MonthlyRepositoryTests.cs | Same test assertion tightening for monthly repository tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/IndexTests.cs | Same test assertion tightening for index maintenance/alias tests. |
| tests/Foundatio.Repositories.Elasticsearch.Tests/DailyRepositoryTests.cs | Same test assertion tightening for daily repository tests. |
| src/Foundatio.Repositories/JsonPatch/PatchDocumentConverter.cs | Preserve inner exception when wrapping invalid patch errors. |
| src/Foundatio.Repositories/JsonPatch/PatchDocument.cs | Fix StreamReader disposal, and add stricter JArray operation element validation. |
| src/Foundatio.Repositories/JsonPatch/Operation.cs | Add null guard to Operation.Build. |
| src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticRepositoryBase.cs | Route cache invalidation through InvalidateCacheAsync and restrict UpdateByQuery fast-path when change handlers are registered. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static PatchDocument Load(Stream document) | ||
| { | ||
| var reader = new StreamReader(document); | ||
| using var reader = new StreamReader(document, leaveOpen: true); |
There was a problem hiding this comment.
new StreamReader(document, leaveOpen: true) is likely not a valid constructor overload for the target frameworks (net8.0/net10.0) unless an encoding parameter is also provided. This may break the build; consider using an overload that explicitly supplies the encoding and leaveOpen (or otherwise ensures the chosen overload exists across TFMs).
| using var reader = new StreamReader(document, leaveOpen: true); | |
| using var reader = new StreamReader(document, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true); |
| public static Operation Build(JObject jOperation) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(jOperation); | ||
| var op = PatchDocument.CreateOperation((string)jOperation["op"]); |
There was a problem hiding this comment.
PatchDocument.CreateOperation can return null for an unknown/missing "op" value; Operation.Build then calls op.Read(...) and will throw a NullReferenceException. Since this method is now adding input validation, it should explicitly validate the "op" token and throw a clear ArgumentException when the operation type is unsupported.
| var op = PatchDocument.CreateOperation((string)jOperation["op"]); | |
| var opName = (string)jOperation["op"]; | |
| if (string.IsNullOrWhiteSpace(opName)) | |
| throw new ArgumentException("The JSON patch operation must contain a non-empty 'op' property.", nameof(jOperation)); | |
| var op = PatchDocument.CreateOperation(opName); | |
| if (op is null) | |
| throw new ArgumentException($"Unsupported JSON patch operation type '{opName}'.", nameof(jOperation)); |
Address PR review: CreateOperation can return null for unknown/missing "op" values, causing NullReferenceException on the subsequent Read call. Now explicitly validates the op property exists and that the operation type is supported, with clear ArgumentException messages. Made-with: Cursor
Replace manual IsNullOrWhiteSpace check with the modern guard clause. Made-with: Cursor
Summary
Cache.RemoveAllAsyncdirectly instead ofInvalidateCacheAsync, bypassing subclass overridesDocumentsChangedhandler check toUpdateByQueryfast-path to prevent silently missed change events when listeners are registeredPatchDocument.Load(Stream)StreamReader resource leak (addusingwithleaveOpen: true)Operation.Buildto preventNullReferenceExceptionon invalid inputPatchDocumentConvertererror handling (was losing root cause and stack trace)PatchDocument.Load(JArray)for clear error messages on malformed patchesAssert.NotNull(x?.Id)into separateAssert.NotNull(x)+Assert.NotNull(x.Id)across all test files (136 instances in 10 files) to prevent false-passing tests when the object itself is nullTest plan