Marten Metadata

A major goal of the Marten V4 release was to enable much richer document and event metadata collection based on user requests. To that end, Marten still supports the same basic metadata columns as Marten V2/V3, but adds other opt in columns.

The available columns for document storage are:

Column NameDescriptionEnabled by Default
mt_last_modifiedTimestamp of the last time the document record was modifiedYes
mt_versionGuid value that marks the current version of the document. This supports optimistic concurrencyYes
mt_dotnet_typeAssembly qualified name of the .Net type persisted to this rowYes
correlation_idUser-supplied correlation identifier (string)No, opt in
causation_idUser-supplied causation identifier (string)No, opt in
headersUser-supplied key/value pairs for extensible metadataNo, opt in
mt_deletedBoolean flag noting whether the document is soft-deletedOnly if the document type is configured as soft-deleted
mt_deleted_atTimestamp marking when a document was soft-deletedOnly if the document type is configured as soft-deleted

Correlation Id, Causation Id, and Headers

TIP

At this point, the Marten team thinks that using a custom ISessionFactory to set correlation and causation data is the most likely usage for this feature. The Marten team plans to build a sample application showing Marten being used with Open Telemetry tracing soon.

The first step is to enable these columns on the document types in your system:

var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");

    // Optionally turn on metadata columns by document type
    opts.Schema.For<User>().Metadata(x =>
    {
        x.CorrelationId.Enabled = true;
        x.CausationId.Enabled = true;
        x.Headers.Enabled = true;
    });

    // Or just globally turn on columns for all document
    // types in one fell swoop
    opts.Policies.ForAllDocuments(x =>
    {
        x.Metadata.CausationId.Enabled = true;
        x.Metadata.CorrelationId.Enabled = true;
        x.Metadata.Headers.Enabled = true;
    });
});

snippet source | anchor

Next, you relay the actual values for these fields at the document session level as shown below:

public void SettingMetadata(IDocumentSession session, string correlationId, string causationId)
{
    // These values will be persisted to any document changed
    // by the session when SaveChanges() is called
    session.CorrelationId = correlationId;
    session.CausationId = causationId;
}

snippet source | anchor

Headers are a little bit different, with the ability to set individual header key/value pairs as shown below:

public void SetHeader(IDocumentSession session, string sagaId)
{
    session.SetHeader("saga-id", sagaId);
}

snippet source | anchor

Tracking Metadata on Documents

Marten can now set metadata values directly on the documents persisted by Marten, but this is an opt in behavior. You can explicitly map a public member of your document type to a metadata value individually. Let's say that you have a document type like this where you want to track metadata:

public class DocWithMetadata
{
    public Guid Id { get; set; }

    // other members

    public Guid Version { get; set; }
    public string Causation { get; set; }
    public bool IsDeleted { get; set; }
}

snippet source | anchor

To enable the Marten mapping to metadata values, use this syntax:

var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");

    // Explicitly map the members on this document type
    // to metadata columns.
    opts.Schema.For<DocWithMetadata>().Metadata(m =>
    {
        m.Version.MapTo(x => x.Version);
        m.CausationId.MapTo(x => x.Causation);
        m.IsSoftDeleted.MapTo(x => x.IsDeleted);
    });
});

snippet source | anchor

TIP

Note that mapping a document member to a metadata column will implicitly enable that metadata column collection.

For correlation, causation, and last modified tracking, an easy way to do this is to just implement the Marten ITracked interface as shown below:

public class MyTrackedDoc: ITracked
{
    public Guid Id { get; set; }
    public string CorrelationId { get; set; }
    public string CausationId { get; set; }
    public string LastModifiedBy { get; set; }
}

snippet source | anchor

If your document type implements this interface, Marten will automatically enable the correlation and causation tracking, and set values for correlation, causation, and the last modified data on documents anytime they are loaded or persisted by Marten.

Likewise, version tracking directly on the document is probably easiest with the IVersioned interface as shown below:

public class MyVersionedDoc: IVersioned
{
    public Guid Id { get; set; }
    public Guid Version { get; set; }
}

snippet source | anchor

Implementing IVersioned will automatically opt your document type into optimistic concurrency checking with mapping of the current version to the IVersioned.Version property.

Disabling All Metadata

If you want Marten to run lean, you can omit all metadata fields from Marten with this configuration:

var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");

    // This will direct Marten to omit all informational
    // metadata fields
    opts.Policies.DisableInformationalFields();
});

snippet source | anchor

Querying by Last Modified

Documents can be queried by the last modified time using these custom extension methods in Marten:

  • ModifiedSince(DateTimeOffset) - Return only documents modified since specific date (not inclusive)
  • ModifiedBefore(DateTimeOffset) - Return only documents modified before specific date (not inclusive)

Here is a sample usage:

public async Task sample_usage(IQuerySession session)
{
    var fiveMinutesAgo = DateTime.UtcNow.AddMinutes(-5);
    var tenMinutesAgo = DateTime.UtcNow.AddMinutes(-10);

    // Query for documents modified between 5 and 10 minutes ago
    var recents = await session.Query<Target>()
        .Where(x => x.ModifiedSince(tenMinutesAgo))
        .Where(x => x.ModifiedBefore(fiveMinutesAgo))
        .ToListAsync();
}

snippet source | anchor

Indexing

See metadata index for information on how to enable predefined indexing