Skip to content

UseOptimisticConcurrency and UseNumericRevisions are asymmetric: call order decides whether the config works or throws #5159

Description

@jeremydmiller

Summary

UseOptimisticConcurrency(true) and UseNumericRevisions(true) are not symmetric in how they clear the other flavor's metadata, so the order the two fluent calls are made in decides whether the configuration silently works or throws at bootstrap.

Behavior today (master, 77efabc0f)

// Order A — last-wins, works.
opts.Schema.For<Foo>().UseOptimisticConcurrency(true);
opts.Schema.For<Foo>().UseNumericRevisions(true);

// Order B — InvalidDocumentException at bootstrap.
opts.Schema.For<Foo>().UseNumericRevisions(true);
opts.Schema.For<Foo>().UseOptimisticConcurrency(true);

Root cause

MartenRegistry.DocumentMappingExpression<T> (src/Marten/MartenRegistry.cs:708-742):

public DocumentMappingExpression<T> UseOptimisticConcurrency(bool enabled)
{
    _builder.Alter = m =>
    {
        m.UseOptimisticConcurrency = enabled;
        if (enabled)
        {
            m.UseNumericRevisions = false;
            m.Metadata.Version.Enabled = true;
            // NOTE: does NOT clear m.Metadata.Revision.Enabled
        }
    };
    return this;
}

public DocumentMappingExpression<T> UseNumericRevisions(bool enabled)
{
    _builder.Alter = m =>
    {
        m.UseNumericRevisions = enabled;
        if (enabled)
        {
            m.UseOptimisticConcurrency = false;
            m.Metadata.Revision.Enabled = true;
            m.Metadata.Version.Enabled = false;   // <-- clears the other flavor
        }
    };
    return this;
}

UseNumericRevisions clears Metadata.Version.Enabled; UseOptimisticConcurrency never clears Metadata.Revision.Enabled. Both flavors then read as enabled, and #5121's new guard in DocumentMapping.CompileAndValidate throws.

Note that VersionedPolicy.Apply already reconciles the pair correctly:

if (mapping.UseOptimisticConcurrency) { Version.Enabled = true;  Revision.Enabled = false; }
if (mapping.UseNumericRevisions)      { Version.Enabled = false; Revision.Enabled = true;  }

So the invalid state is only reachable through a fluent Alter, which runs after the policies.

Why this matters beyond call ordering

The same asymmetry means UseOptimisticConcurrency(true) throws on any type that already has numeric revisions from a policy rather than from an explicit call — it implements IRevisioned/ILongVersioned, carries a [Version] member, or is an aggregate-projection target. In those cases nothing in the user's configuration says "numeric revisions", so the error reads as though Marten invented a conflict. (Covered by interface_driven_revisions_plus_optimistic_concurrency_fails_fast in #5157.)

Proposed direction

Two changes that compose:

  1. Make UseOptimisticConcurrency(true) clear Metadata.Revision.Enabled, mirroring UseNumericRevisions. Both orders then become last-wins for ordinary documents, which is what the fluent API reads like it should do.
  2. Narrow the Derive the StreamOne ETag from the numeric revision for revisioned documents #5121 guard so it fires on the case that genuinely cannot work rather than on "both flags happen to be set" — an aggregate-projection target being pushed onto Guid concurrency, detectable via StoreOptions.Projections.TryFindAggregate(DocumentType, out _). That is the actual Do not allow projected documents to be configured as requiring optimistic concurrency #2978 rule, and it keeps the actionable error where it belongs while letting plain documents override freely.

Worth deciding explicitly whether (1) should also apply to interface-driven revisions (IRevisioned + an explicit UseOptimisticConcurrency(true)), or whether that should keep failing fast — the interface's Version member would be left unmapped, so continuing to throw there is defensible.

Context

Not a regression: before #5121 both orders reached the database as DDL with two mt_version columns and failed with MartenSchemaException: DDL Execution for 'All Configured Changes' Failed!. Verified against master at de63fc5ed. #5121 turned that into an actionable InvalidDocumentException; this issue is about the underlying asymmetry it surfaced.

Raised while reviewing #5121, deliberately left out of #5157 because it changes document-configuration behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions