Skip to content

Fix code quality: cache invalidation, resource disposal, null safety, and test assertions#218

Merged
niemyjski merged 3 commits intomainfrom
fix/code-quality
Mar 2, 2026
Merged

Fix code quality: cache invalidation, resource disposal, null safety, and test assertions#218
niemyjski merged 3 commits intomainfrom
fix/code-quality

Conversation

@niemyjski
Copy link
Member

Summary

  • Fix cache invalidation inconsistency: two scroll-based batch paths used Cache.RemoveAllAsync directly instead of InvalidateCacheAsync, bypassing subclass overrides
  • Add DocumentsChanged handler check to UpdateByQuery fast-path to prevent silently missed change events when listeners are registered
  • Fix PatchDocument.Load(Stream) StreamReader resource leak (add using with leaveOpen: true)
  • Add null guard to Operation.Build to prevent NullReferenceException on invalid input
  • Preserve inner exception in PatchDocumentConverter error handling (was losing root cause and stack trace)
  • Add explicit type validation in PatchDocument.Load(JArray) for clear error messages on malformed patches
  • Split Assert.NotNull(x?.Id) into separate Assert.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 null

Test plan

  • Core repository tests pass (47/47)
  • Elasticsearch integration tests pass (requires running ES instance in CI)
  • Build produces zero errors and zero new warnings

- 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
Copy link
Member Author

@niemyjski niemyjski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
using var reader = new StreamReader(document, leaveOpen: true);
using var reader = new StreamReader(document, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);

Copilot uses AI. Check for mistakes.
public static Operation Build(JObject jOperation)
{
ArgumentNullException.ThrowIfNull(jOperation);
var op = PatchDocument.CreateOperation((string)jOperation["op"]);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
@niemyjski niemyjski self-assigned this Mar 1, 2026
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
@niemyjski niemyjski merged commit 3241e59 into main Mar 2, 2026
4 checks passed
@niemyjski niemyjski deleted the fix/code-quality branch March 2, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants