Skip to content

Document Identity

Besides being serializable, Marten's only other requirement for a .Net type to be a document is the existence of an identifier field or property that Marten can use as the primary key for the document type. The Id can be either a public field or property, and the name must be either id or Id or ID. As of this time, Marten supports these Id types:

  1. String. It might be valuable to use a natural key as the identifier, especially if it is valuable within the Identity Map feature of Marten Db. In this case, the user will be responsible for supplying the identifier.
  2. Guid. If the id is a Guid, Marten will assign a new value for you when you persist the document for the first time if the id is empty. And for the record, it's pronounced "gwid".
  3. CombGuid is a sequential Guid algorithm. It can improve performance over the default Guid as it reduces fragmentation of the PK index.
  4. Int or Long. As of right now, Marten uses a HiLo generator approach to assigning numeric identifiers by document type. Marten may support Postgresql sequences or star-based algorithms as later alternatives.
  5. Strong Typed Identifiers where a type like the C# public record struct NewGuidId(Guid Value); wraps an inner int, long, Guid, or string value
  6. A single-case F# discriminated union can be used as the identifier. In this case, only Guid and string can be used as inner types, and you must set the ID yourself before saving the document (the ID cannot be automatically set by Marten since F# records are immutable and do not have setters - this would require deep copying arbitrary document types dynamically which is a relatively complex feature not supported at this time)
  7. When the ID member of a document is not settable or not public a NoOpIdGeneration strategy is used. This ensures that Marten does not set the ID itself, so the ID should be generated manually.
  8. A Custom ID generator strategy is used to implement the ID generation strategy yourself.

Marten by default uses the identity value set on documents and only assigns one in case it has no value (Guid.Empty, 0, string.Empty etc).

INFO

When using a Guid/CombGuid, Int, or Long identifier, Marten will ensure the identity is set immediately after calling IDocumentSession.Store on the entity.

You can see some example id usages below:

cs
public class Division
{
    // String property as Id
    public string Id { get; set; }
}

public class Category
{
    // Guid's work, fields too
    public Guid Id;
}

public class Invoice
{
    // int's and long's can be the Id
    // "id" is accepted
    public int id { get; set; }
}

snippet source | anchor

Overriding the Choice of Id Property/Field

If you really want to, or you're migrating existing document types from another document database, Marten provides the [Identity] attribute to force Marten to use a property or field as the identifier that doesn't match the "id" or "Id" or "ID" convention:

cs
public class NonStandardDoc
{
    [Identity]
    public string Name;
}

snippet source | anchor

The identity property or field can also be configured through StoreOptions by using the Schema to obtain a document mapping:

cs
storeOptions.Schema.For<OverriddenIdDoc>().Identity(x => x.Name);

snippet source | anchor

Guid Identifiers

INFO

As of Marten 1.0, the default Guid mechanism is a sequential or "Comb" Guid. While more expensive to generate, this makes inserts into the underlying document tables more efficient.

To use CombGuid generation you should enable it when configuring the document store. This defines that the CombGuid generation strategy will be used for all the documents types.

cs
options.Policies.ForAllDocuments(m =>
{
    if (m.IdType == typeof(Guid))
    {
        m.IdStrategy = new CombGuidIdGeneration();
    }
});

snippet source | anchor

It is also possible use the SequentialGuid id generation algorithm for a specific document type.

cs
options.Schema.For<UserWithGuid>().IdStrategy(new CombGuidIdGeneration());

snippet source | anchor

Sequential Identifiers with Hilo

The Hilo sequence generation can be customized with either global defaults or document type-specific overrides. By default, the Hilo sequence generation in Marten increments by 1 and uses a "maximum lo" number of 1000.

To set different global defaults, use the StoreOptions.HiloSequenceDefaults property like this sample:

cs
var store = DocumentStore.For(_ =>
{
    _.Advanced.HiloSequenceDefaults.MaxLo = 55;
    _.Connection(ConnectionSource.ConnectionString);
    _.DatabaseSchemaName = "sequences";
});

snippet source | anchor

It's also possible to use one sequence with multiple document types by specifying the same "sequence name".

cs
var store = DocumentStore.For(_ =>
{
    _.Advanced.HiloSequenceDefaults.SequenceName = "Entity";
    _.Connection(ConnectionSource.ConnectionString);

    _.DatabaseSchemaName = "sequences";
});

snippet source | anchor

To override the Hilo configuration for a specific document type, you can decorate the document type with the [HiloSequence] attribute as in this example:

cs
[HiloSequence(MaxLo = 66, SequenceName = "Entity")]
public class OverriddenHiloDoc
{
    public int Id { get; set; }
}

snippet source | anchor

You can also use the MartenRegistry fluent interface to override the Hilo configuration for a document type as in this example:

cs
var store = DocumentStore.For(_ =>
{
    // Overriding the Hilo settings for the document type "IntDoc"
    _.Schema.For<IntDoc>()
        .HiloSettings(new HiloSettings {MaxLo = 66});

    _.Connection(ConnectionSource.ConnectionString);

    _.DatabaseSchemaName = "sequences";
});

snippet source | anchor

Set the HiLo Identifier Floor

Marten 1.2 adds a convenience method to reset the "floor" of the Hilo sequence for a single document type:

cs
var store = DocumentStore.For(opts =>
{
    opts.Connection(ConnectionSource.ConnectionString);
    opts.DatabaseSchemaName = "sequences";
});

// Resets the minimum Id number for the IntDoc document
// type to 2500
await store.Tenancy.Default.Database.ResetHiloSequenceFloor<IntDoc>(2500);

snippet source | anchor

This functionality was added specifically to aid in importing data from an existing data source. Do note that this functionality simply guarantees that all new IDs assigned for the document type will be higher than the new floor. It is perfectly possible, and even likely, that there will be some gaps in the id sequence.

String Identity

If you use a document type with a string identity member, you will be responsible for supplying the identity value to Marten on any object passed to any storage API like IDocumentSession.Store(). You can choose to use the Identity Key option for automatic identity generation as shown in the next section.

Identity Key

WARNING

The document alias is also used to name the underlying Postgresql table and functions for this document type, so you will not be able to use any kind of punctuation characters or spaces.

Let's say you have a document type with a string for the identity member like this one:

cs
public class DocumentWithStringId
{
    public string Id { get; set; }
}

snippet source | anchor

You can use the "identity key" option for identity generation that would create string values of the pattern [type alias]/[sequence] where the type alias is typically the document class name in all lower case and the sequence is a HiLo sequence number.

You can opt into the identity key strategy for identity and even override the document alias name with this syntax:

cs
var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");
    opts.Schema.For<DocumentWithStringId>()
        .UseIdentityKey()
        .DocumentAlias("doc");
});

snippet source | anchor

Custom Identity Strategies

A custom ID generator strategy should implement IIdGeneration.

cs
public class CustomIdGeneration : IIdGeneration
{
    public IEnumerable<Type> KeyTypes { get; } = new Type[] {typeof(string)};

    public bool RequiresSequences { get; } = false;
    public void GenerateCode(GeneratedMethod assign, DocumentMapping mapping)
    {
        var document = new Use(mapping.DocumentType);
        assign.Frames.Code($"_setter({{0}}, \"newId\");", document);
        assign.Frames.Code($"return {{0}}.{mapping.CodeGen.AccessId};", document);
    }

}

snippet source | anchor

The Build() method should return the actual IdGenerator<T> for the document type, where T is the type of the Id field.

For more advances examples you can have a look at existing ID generator: HiloIdGeneration, CombGuidGenerator and the IdentityKeyGeneration,

To use custom id generation you should enable it when configuring the document store. This defines that the strategy will be used for all the documents types.

cs
options.Policies.ForAllDocuments(m =>
{
    if (m.IdType == typeof(string))
    {
        m.IdStrategy = new CustomIdGeneration();
    }
});

snippet source | anchor

It is also possible define a custom id generation algorithm for a specific document type.

cs
options.Schema.For<UserWithString>().IdStrategy(new CustomIdGeneration());

snippet source | anchor

Strong Typed Identifiers 7.20

WARNING

There are lots of rules in Marten about what can and what can't be used as a strong typed identifier, and this documentation is trying hard to explain them, but you're best off copying the examples and using something like either Vogen or StronglyTypedID for now.

INFO

There is not yet any direct support for strong typed identifiers for the event store

Marten can now support strong typed identifiers using a couple different strategies. As of this moment, Marten can automatically use types that conform to one of two patterns:

cs
// Use a constructor for the inner value,
// and expose the inner value in a *public*
// property getter
public record struct TaskId(Guid Value);

/// <summary>
/// Pair a public property getter for the inner value
/// with a public static method that takes in the
/// inner value
/// </summary>
public struct Task2Id
{
    private Task2Id(Guid value) => Value = value;

    public Guid Value { get; }

    public static Task2Id From(Guid value) => new Task2Id(value);
}

snippet source | anchor

In all cases, the type name will have to be suffixed with "Id" (and it's case sensitive) to be considered by Marten to be a strong typed identity type. The identity types will also need to be immutable struct types

The property names or static "builder" methods do not have any requirements for the names, but Value and From are common. So far, Marten's strong typed identifier support has been tested with:

  1. Hand rolled types, but there's some advantage to using the next two options for JSON serialization, comparisons, and plenty of other goodness
  2. Vogen
  3. StronglyTypedId

Jumping right into an example, let's say that we want to use this identifier with Vogen for a Guid-wrapped identifier:

cs
[ValueObject<Guid>]
public partial struct InvoiceId;

public class Invoice
{
    // Marten will use this for the identifier
    // of the Invoice document
    public InvoiceId? Id { get; set; }
    public string Name { get; set; }
}

snippet source | anchor

The usage of our Invoice document is essentially the same as a document type with the primitive identifier types:

cs
[Fact]
public async Task update_a_document_smoke_test()
{
    var invoice = new Invoice();

    // Just like you're used to with other identity
    // strategies, Marten is able to assign an identity
    // if none is provided
    theSession.Insert(invoice);
    await theSession.SaveChangesAsync();

    invoice.Name = "updated";
    await theSession.SaveChangesAsync();

    // This is a new overload
    var loaded = await theSession.LoadAsync<Invoice>(invoice.Id);
    loaded.Name.ShouldBeNull("updated");
}

snippet source | anchor

As you might infer -- or not -- there's a couple rules and internal behavior:

  • The identity selection is done just the same as the primitive types, Marten is either looking for an id/Id member, or a member decorated with [Identity]
  • If Marten is going to assign the identity, you will need to use Nullable<T> for the identity member of the document
  • There is a new IQuerySession.LoadAsync<T>(object id) overload that was specifically built for strong typed identifiers
  • For Guid-wrapped values, Marten is assigning missing identity values based on its sequential Guid support
  • For int or long-wrapped values, Marten is using its HiLo support to define the wrapped values
  • For string-wrapped values, Marten is going to require you to assign the identity to documents yourself

For another example, here's a usage of an int wrapped identifier:

cs
[StronglyTypedId(Template.Int)]
public partial struct Order2Id;

public class Order2
{
    public Order2Id? Id { get; set; }
    public string Name { get; set; }
}

snippet source | anchor

WARNING

Sorry folks, only the asynchronous APIs for loading documents are supported for strong typed identifiers

As of now, Marten supports:

  • Loading a single document by its identifier
  • Loading multiple documents using IsOneOf() as shown below:

cs
[Fact]
public async Task load_many()
{
    var issue1 = new Issue2{Name = Guid.NewGuid().ToString()};
    var issue2 = new Issue2{Name = Guid.NewGuid().ToString()};
    var issue3 = new Issue2{Name = Guid.NewGuid().ToString()};
    theSession.Store(issue1, issue2, issue3);

    await theSession.SaveChangesAsync();

    var results = await theSession.Query<Issue2>()
        .Where(x => x.Id.IsOneOf(issue1.Id, issue2.Id, issue3.Id))
        .ToListAsync();

    results.Count.ShouldBe(3);
}

snippet source | anchor

WARNING

LoadManyAsync()_ is not supported for strong typed identifiers

  • Deleting a document by identity
  • Deleting a document by the document itself
  • Within Include() queries:

cs
[Fact]
public async Task include_a_single_reference()
{
    var teacher = new Teacher();
    var c = new Class();

    theSession.Store(teacher);

    c.TeacherId = teacher.Id;
    theSession.Store(c);

    await theSession.SaveChangesAsync();

    theSession.Logger = new TestOutputMartenLogger(_output);

    var list = new List<Teacher>();

    var loaded = await theSession
        .Query<Class>()
        .Include<Teacher>(c => c.TeacherId, list)
        .Where(x => x.Id == c.Id)
        .FirstOrDefaultAsync();

    loaded.Id.ShouldBe(c.Id);
    list.Single().Id.ShouldBe(teacher.Id);
}

snippet source | anchor

  • Within LINQ Where() clauses
  • Within LINQ Select() clauses
  • Within LINQ OrderBy() clauses
  • Identity map resolution
  • Automatic dirty checks

LINQ Support

There's a possible timing issue with the strong typed identifiers. Every time that Marten evaluates the identity strategy for a document that uses a strong typed identifier, Marten "remembers" that that type is a custom value type and will always treat any usage of that value type as being the actual wrapped value when constructing any SQL. You might need to help out Marten a little bit by telling Marten ahead of time about value types before it tries to evaluate any LINQ expressions that use members that are value types like so:

cs
[ValueObject<int>]
public partial struct UpperLimit;

[ValueObject<int>]
public partial struct LowerLimit;

public class LimitedDoc
{
    public Guid Id { get; set; }
    public UpperLimit Upper { get; set; }
    public LowerLimit Lower { get; set; }
}

snippet source | anchor

And the UpperLimit and LowerLimit value types can be registered with Marten like so:

cs
// opts is a StoreOptions just like you'd have in
// AddMarten() calls
opts.RegisterValueType(typeof(UpperLimit));
opts.RegisterValueType(typeof(LowerLimit));

snippet source | anchor

And that will enable you to seamlessly use the value types in LINQ expressions like so:

cs
[Fact]
public async Task store_several_and_order_by()
{
    var doc1 = new LimitedDoc { Lower = LowerLimit.From(1), Upper = UpperLimit.From(20) };
    var doc2 = new LimitedDoc { Lower = LowerLimit.From(5), Upper = UpperLimit.From(25) };
    var doc3 = new LimitedDoc { Lower = LowerLimit.From(4), Upper = UpperLimit.From(15) };
    var doc4 = new LimitedDoc { Lower = LowerLimit.From(3), Upper = UpperLimit.From(10) };

    theSession.Store(doc1, doc2, doc3, doc4);
    await theSession.SaveChangesAsync();

    var ordered = await theSession
        .Query<LimitedDoc>()
        .OrderBy(x => x.Lower)
        .Select(x => x.Id)
        .ToListAsync();

    ordered.ShouldHaveTheSameElementsAs(doc1.Id, doc4.Id, doc3.Id, doc2.Id);
}

snippet source | anchor

Released under the MIT License.