Skip to content

DeleteOperation.OldDocument is hardcoded to null in 0.5.5 — breaks soft-delete conversion and delete-side interceptors #27

Description

@kerem-acer

Summary

In the published 0.5.5 package (currently the latest on NuGet), DeleteOperation<TDocument>.OldDocument returns null unconditionally, ignoring the document that was passed to DocumentSet.Delete(entity). This silently breaks any logic that inspects a delete operation's OldDocument — most importantly the built-in soft delete, and any consumer interceptor that gates on the deleted document.

The fix is already present on main (public override object? OldDocument => _document;) but has not been published — 0.5.5 is the newest version on NuGet.

Root cause

Disassembly of the published MongoFlow 0.5.5 assembly:

DeleteOperation`1::get_OldDocument():
    IL_0000: ldnull
    IL_0001: ret

OldDocument returns a hardcoded null, even though DocumentSet.Delete(TDocument document) correctly constructs new DeleteOperation<TDocument>(filter, document, ...) and stores _document = document.

Current main (correct):

// src/Core/Operations/DeleteOperation.cs
public override object? OldDocument => _document;

Impact 1 — built-in soft delete never fires (silent hard delete)

SoftDeleteInterceptor<TSoftDeleteInterface>.SavingChangesAsync decides whether to convert a delete into an update by testing OldDocument:

if (operation.OperationType is OperationType.Delete
    && operation.OldDocument is TSoftDeleteInterface softDeleteDocument)
{
    _options.ChangeIsDeleted(softDeleteDocument, true);
    // ... convert Delete -> Update
}

With OldDocument == null, null is TSoftDeleteInterface is always false, so the branch is never taken, no exception is thrown, and the row is hard-deleted even though AddSoftDelete was configured. The soft-delete query filter works correctly (a row flagged IsDeleted = true is hidden from reads and revealed by DisableSoftDelete()); only the delete → update conversion is broken.

Impact 2 — delete-side interceptors silently skip deletes

Any consumer VaultInterceptor that reads operation.OldDocument for OperationType.Delete operations is also affected. For example, an authorization interceptor that gates delete permission checks like this:

case { OperationType: OperationType.Delete, OldDocument: not null }:
    yield return (operation.OldDocument, PermissionDatabaseAction.Delete);

never matches, so delete operations are silently not authorization-checked. This is a security-relevant footgun for downstream consumers using MongoFlow as their authorization enforcement point.

Reproduction

  1. Define class Thing : ISoftDelete { public bool Deleted { get; set; } ... }.
  2. Configure builder.AddSoftDelete(new VaultSoftDeleteOptions<ISoftDelete> { IsDeletedAccessor = x => x.Deleted, ChangeIsDeleted = (e, v) => e.Deleted = v });.
  3. Add and save a Thing.
  4. Load it, vault.Things.Delete(thing), SaveAsync().
  5. Expected: the document remains with Deleted = true. Actual (0.5.5): the document is physically removed.

Verified against real MongoDB in an integration test: DisableSoftDelete().Find(...) returns 0 rows and a raw driver query confirms the document is gone.

Fix

main already returns _document from OldDocument; please cut a release (e.g. 0.5.6) so the fix is consumable from NuGet. Until then, 0.5.5 consumers relying on soft delete or delete-side interceptor logic are silently affected.


Found while building real-MongoDB integration tests for MongoFlow's cross-cutting vault behavior (multi-tenancy + soft-delete). The tenant-isolation guarantees pass; this is the one behavior that surfaced the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions