Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/documents/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ Marten will not perfectly keep incrementing the IRevisioned.Revision number if t
same session. Prefer using `UpdateRevision()` if you try to continuously update the same document from the same session!
:::

::: tip `IRevisioned` (int) vs `ILongVersioned` (long)
`IRevisioned.Version` is an `int` — the right choice for an ordinary per-document revision counter. For documents projected from a `MultiStreamProjection` whose `Version` is the global **event sequence number**, the value can exceed `Int32.MaxValue`; implement `ILongVersioned` (with a `long Version`) instead. Both opt the document into numeric revisioning and use the same `bigint` `mt_version` column; the only difference is the member width. A `MultiStreamProjection`-derived document that implements `IRevisioned` (int) will overflow on the `bigint → int` read once its version exceeds `Int32` — use `ILongVersioned` for those.
:::

or finally by adding the `[Version]` attribute to a public member on the document type to opt into the
`UseNumericRevisions` behavior on the parent type with the decorated member being tracked as the version number as
shown in this sample:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ public async Task insert_collision_throws_DocumentAlreadyExists_under_numeric()
}
}

public class RevDoc: Marten.Metadata.IRevisioned
public class RevDoc: IRevisioned
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public long Version { get; set; }
public int Version { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task start_as_inline_move_to_async_and_just_continue()
public class SimpleAggregate : IRevisioned
{
// This will be the aggregate version
public long Version { get; set; }
public int Version { get; set; }

public Guid Id { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions src/DaemonTests/Aggregations/side_effects_in_aggregations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ public class SideEffects1: IRevisioned
public int B { get; set; }
public int C { get; set; }
public int D { get; set; }
public long Version { get; set; }
public int Version { get; set; }
}

public record WasDeleted(Guid Id);
Expand Down Expand Up @@ -460,7 +460,7 @@ public class SideEffects2: IRevisioned
public int B { get; set; }
public int C { get; set; }
public int D { get; set; }
public long Version { get; set; }
public int Version { get; set; }
}

public class RecordingMessageOutbox: IMessageOutbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public class Bug4428Counter: IRevisioned
public Guid Id { get; set; }
public int Increments { get; set; }
public int Bonuses { get; set; }
public long Version { get; set; }
public int Version { get; set; }
}

public partial class Bug4428CounterProjection: SingleStreamProjection<Bug4428Counter, Guid>
Expand Down
2 changes: 1 addition & 1 deletion src/DaemonTests/TestingSupport/Trip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override string ToString()
return $"{nameof(Id)}: {Id}, {nameof(EndedOn)}: {EndedOn}, {nameof(Traveled)}: {Traveled}, {nameof(State)}: {State}, {nameof(Active)}: {Active}, {nameof(StartedOn)}: {StartedOn}";
}

public long Version { get; set; }
public int Version { get; set; }
}


Expand Down
46 changes: 46 additions & 0 deletions src/DocumentDbTests/Concurrency/long_versioned_revisioning.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using Marten.Schema;
using Marten.Testing.Harness;
using Shouldly;
using Xunit;

namespace DocumentDbTests.Concurrency;

public class long_versioned_revisioning: OneOffConfigurationsContext
{
[Fact]
public void infer_numeric_revisioning_from_ILongVersioned()
{
var mapping = (DocumentMapping)theStore.Options.Storage.FindMapping(typeof(LongVersionedDoc));
mapping.UseNumericRevisions.ShouldBeTrue();
mapping.Metadata.Revision.Enabled.ShouldBeTrue();
mapping.Metadata.Revision.Member.Name.ShouldBe("Version");
}

[Fact]
public async Task round_trips_a_version_greater_than_int32()
{
// #4528: an ILongVersioned document carries the 64-bit revision (e.g. a
// MultiStreamProjection's event sequence number). The bigint mt_version column
// must round-trip a value > Int32.MaxValue without the truncation an int
// IRevisioned member would suffer.
var doc = new LongVersionedDoc { Id = Guid.NewGuid(), Name = "big" };
var bigVersion = (long)int.MaxValue + 12345;

theSession.UpdateRevision(doc, bigVersion);
await theSession.SaveChangesAsync();

await using var query = theStore.QuerySession();
var loaded = await query.LoadAsync<LongVersionedDoc>(doc.Id);
loaded.ShouldNotBeNull();
loaded.Version.ShouldBe(bigVersion);
}
}

public class LongVersionedDoc: ILongVersioned
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public long Version { get; set; }
}
3 changes: 1 addition & 2 deletions src/DocumentDbTests/Concurrency/numeric_revisioning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using Shouldly;
using Weasel.Core;
using Xunit;
using IRevisioned = Marten.Metadata.IRevisioned;

namespace DocumentDbTests.Concurrency;

Expand Down Expand Up @@ -567,7 +566,7 @@ public class RevisionedDoc: IRevisioned
public Guid Id { get; set; }
public string Name { get; set; }

public long Version { get; set; }
public int Version { get; set; }
}

public class OtherRevisionedDoc
Expand Down
2 changes: 1 addition & 1 deletion src/DocumentDbTests/Writing/bulk_loading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,6 @@ public class RevisionedBulkDoc : IRevisioned
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public long Version { get; set; }
public int Version { get; set; }
}
}
6 changes: 3 additions & 3 deletions src/EventSourcingTests/Aggregation/stream_compacting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ public class Letters : IRevisioned
public int BCount { get; set; }
public int CCount { get; set; }
public int DCount { get; set; }
public long Version { get; set; }
public int Version { get; set; }
}

public class LetterCounts: IRevisioned
Expand All @@ -372,7 +372,7 @@ public class LetterCounts: IRevisioned
public int BCount { get; set; }
public int CCount { get; set; }
public int DCount { get; set; }
public long Version { get; set; }
public int Version { get; set; }
}

public class LetterCountsByString: IRevisioned
Expand All @@ -382,7 +382,7 @@ public class LetterCountsByString: IRevisioned
public int BCount { get; set; }
public int CCount { get; set; }
public int DCount { get; set; }
public long Version { get; set; }
public int Version { get; set; }
}

public partial class LetterCountsByStringProjection: SingleStreamProjection<LetterCountsByString, string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ public async Task finding_last_aggregate_using_string()

public record DeleteYourself;

public class SimpleMaybeDeletedAggregate : Marten.Metadata.IRevisioned
public class SimpleMaybeDeletedAggregate : IRevisioned
{
// This will be the aggregate version
public long Version { get; set; }
public int Version { get; set; }

public bool ShouldDelete(DeleteYourself _) => true;

Expand Down Expand Up @@ -160,7 +160,7 @@ public override string ToString()
}
}

public class SimpleAsStringMaybeDeletedAggregate : Marten.Metadata.IRevisioned
public class SimpleAsStringMaybeDeletedAggregate : IRevisioned
{
protected bool Equals(SimpleAsStringMaybeDeletedAggregate other)
{
Expand Down Expand Up @@ -193,7 +193,7 @@ public override int GetHashCode()
}

// This will be the aggregate version
public long Version { get; set; }
public int Version { get; set; }

public bool ShouldDelete(DeleteYourself _) => true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
using Marten.Testing.Harness;
using Shouldly;
using Xunit;
using IRevisioned = Marten.Metadata.IRevisioned;

namespace EventSourcingTests.FetchForWriting;

Expand Down Expand Up @@ -776,7 +775,7 @@ public async Task work_correctly_for_multiple_calls_with_identity_map()
public class SimpleAggregate : IRevisioned
{
// This will be the aggregate version
public long Version { get; set; }
public int Version { get; set; }

public Guid Id { get;
set; }
Expand Down Expand Up @@ -987,13 +986,13 @@ public class SomeProjection : IRevisioned
public Guid Id { get; set; }
public int A { get; set; }
public void Apply(EventA e) => A++;
public long Version { get; set; }
public int Version { get; set; }
}

public class SomeOtherProjection : IRevisioned
{
public Guid Id { get; set; }
public int A { get; set; }
public void Apply(EventA e) => A++;
public long Version { get; set; }
public int Version { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ public async Task does_not_create_tables()

#region sample_using_simple_explicit_code_for_live_aggregation

public class CountedAggregate: Marten.Metadata.IRevisioned
public class CountedAggregate: IRevisioned
{
// This will be the aggregate version
public long Version { get; set; }
public int Version { get; set; }

public Guid Id
{
Expand Down
2 changes: 1 addition & 1 deletion src/Marten.Testing/Examples/RevisionedDocuments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public class Reservation: IRevisioned

// other properties

public long Version { get; set; }
public int Version { get; set; }
}

#endregion
15 changes: 14 additions & 1 deletion src/Marten/Internal/ClosedShape/DocumentRevisionBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@ public DocumentRevisionBinder(MemberInfo? revisionMember)
{
if (revisionMember is not null)
{
_setter = LambdaBuilder.Setter<TDoc, long>(revisionMember);
// #4526/#4528: the mt_version column is bigint, but the document member is
// either an int (IRevisioned) or a long (ILongVersioned). For an int member
// downcast the loaded long; this can overflow for huge MultiStreamProjection
// event sequences — those documents should use ILongVersioned (documented
// caveat).
if (revisionMember.GetRawMemberType() == typeof(int))
{
var intSetter = LambdaBuilder.Setter<TDoc, int>(revisionMember);
_setter = (doc, revision) => intSetter(doc, (int)revision);
}
else
{
_setter = LambdaBuilder.Setter<TDoc, long>(revisionMember);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/Marten/Internal/Sessions/DocumentSessionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ private void storeEntity<T>(T entity, IDocumentStorage<T> storage) where T : not
case IRevisioned revisioned when revisioned.Version != 0:
storage.Store(this, entity, revisioned.Version);
return;
case ILongVersioned longVersioned when longVersioned.Version != 0:
storage.Store(this, entity, longVersioned.Version);
return;
default:
// Put it in the identity map -- if necessary
storage.Store(this, entity);
Expand Down
15 changes: 0 additions & 15 deletions src/Marten/Metadata/IRevisioned.cs

This file was deleted.

10 changes: 10 additions & 0 deletions src/Marten/Metadata/VersionedPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ public void Apply(DocumentMapping mapping)
mapping.Metadata.Revision.Member = mapping.DocumentType.GetProperty(nameof(IRevisioned.Version));
}

// #4528: ILongVersioned is the 64-bit revision variant for documents projected
// from a MultiStreamProjection, where Version is the global event sequence number
// and can exceed Int32.
else if (mapping.DocumentType.CanBeCastTo<ILongVersioned>())
{
mapping.UseNumericRevisions = true;
mapping.Metadata.Revision.Enabled = true;
mapping.Metadata.Revision.Member = mapping.DocumentType.GetProperty(nameof(ILongVersioned.Version));
}

if (mapping.UseOptimisticConcurrency)
{
mapping.Metadata.Version.Enabled = true;
Expand Down
9 changes: 8 additions & 1 deletion src/Marten/Storage/Metadata/MetadataColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public override MemberInfo Member
{
if (value != null)
{
if (value.GetRawMemberType() != typeof(T))
if (!IsAcceptableMemberType(value.GetRawMemberType()!))
{
throw new ArgumentOutOfRangeException(nameof(value),
$"The {_memberName} member of {value.DeclaringType?.NameInCode() ?? "null"} has to be of type {typeof(T).NameInCode()}");
Expand All @@ -114,6 +114,13 @@ public override MemberInfo Member
}
}

/// <summary>
/// Whether a document member of the given type can back this metadata column.
/// Defaults to an exact match on <typeparamref name="T"/>; the revision column
/// overrides this to accept both int (IRevisioned) and long (ILongVersioned).
/// </summary>
protected virtual bool IsAcceptableMemberType(Type memberType) => memberType == typeof(T);

internal override async Task ApplyAsync(IMartenSession martenSession, DocumentMetadata metadata, int index,
DbDataReader reader, CancellationToken token)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Marten/Storage/Metadata/RevisionColumn.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using JasperFx.Core;
using Marten.Internal.CodeGeneration;
using Marten.Internal.Sessions;
Expand All @@ -23,6 +24,12 @@ internal override UpsertArgument ToArgument()
return new RevisionArgument();
}

// #4526/#4528: the bigint mt_version column backs either an int IRevisioned.Version
// or a long ILongVersioned.Version member. The bigint<->member conversion is handled
// by DocumentRevisionBinder (read) and the closed-shape operations (write).
protected override bool IsAcceptableMemberType(Type memberType)
=> memberType == typeof(long) || memberType == typeof(int);

public bool ShouldSelect(DocumentMapping mapping, StorageStyle storageStyle)
{
if (Member != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ public class DocThatShouldBeExempted2
public class SimpleAggregate : IRevisioned
{
// This will be the aggregate version
public long Version { get; set; }
public int Version { get; set; }

public Guid Id { get; set; }

Expand Down
5 changes: 5 additions & 0 deletions src/Shared/DedupeAliases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
// TenancyStyle + DeleteStyle -> JasperFx (jasperfx#327 / marten#4517)
global using TenancyStyle = JasperFx.MultiTenancy.TenancyStyle;
global using DeleteStyle = JasperFx.DeleteStyle;
// IRevisioned reverted to JasperFx's canonical int signature; ILongVersioned (long)
// added for MultiStreamProjection-derived docs whose Version is the event sequence
// (jasperfx#348 / marten#4526 / marten#4528).
global using IRevisioned = JasperFx.IRevisioned;
global using ILongVersioned = JasperFx.ILongVersioned;
// Metadata markers -> JasperFx.Metadata (jasperfx#330 / marten#4520)
global using ISoftDeleted = JasperFx.Metadata.ISoftDeleted;
global using IVersioned = JasperFx.Metadata.IVersioned;
Expand Down
Loading