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:
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.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".CombGuid
is a sequential Guid algorithm. It can improve performance over the default Guid as it reduces fragmentation of the PK index.Int
orLong
. 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.- Strong Typed Identifiers where a type like the C#
public record struct NewGuidId(Guid Value);
wraps an innerint
,long
,Guid
, orstring
value - A single-case F# discriminated union can be used as the identifier. In this case, only
Guid
andstring
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) - 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. - 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:
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; }
}
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:
public class NonStandardDoc
{
[Identity]
public string Name;
}
The identity property or field can also be configured through StoreOptions
by using the Schema
to obtain a document mapping:
storeOptions.Schema.For<OverriddenIdDoc>().Identity(x => x.Name);
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.
options.Policies.ForAllDocuments(m =>
{
if (m.IdType == typeof(Guid))
{
m.IdStrategy = new CombGuidIdGeneration();
}
});
It is also possible use the SequentialGuid id generation algorithm for a specific document type.
options.Schema.For<UserWithGuid>().IdStrategy(new CombGuidIdGeneration());
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:
var store = DocumentStore.For(_ =>
{
_.Advanced.HiloSequenceDefaults.MaxLo = 55;
_.Connection(ConnectionSource.ConnectionString);
_.DatabaseSchemaName = "sequences";
});
It's also possible to use one sequence with multiple document types by specifying the same "sequence name".
var store = DocumentStore.For(_ =>
{
_.Advanced.HiloSequenceDefaults.SequenceName = "Entity";
_.Connection(ConnectionSource.ConnectionString);
_.DatabaseSchemaName = "sequences";
});
To override the Hilo configuration for a specific document type, you can decorate the document type with the [HiloSequence]
attribute as in this example:
[HiloSequence(MaxLo = 66, SequenceName = "Entity")]
public class OverriddenHiloDoc
{
public int Id { get; set; }
}
You can also use the MartenRegistry
fluent interface to override the Hilo configuration for a document type as in this example:
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";
});
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:
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);
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:
public class DocumentWithStringId
{
public string Id { get; set; }
}
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:
var store = DocumentStore.For(opts =>
{
opts.Connection("some connection string");
opts.Schema.For<DocumentWithStringId>()
.UseIdentityKey()
.DocumentAlias("doc");
});
Custom Identity Strategies
A custom ID generator strategy should implement IIdGeneration.
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);
}
}
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.
options.Policies.ForAllDocuments(m =>
{
if (m.IdType == typeof(string))
{
m.IdStrategy = new CustomIdGeneration();
}
});
It is also possible define a custom id generation algorithm for a specific document type.
options.Schema.For<UserWithString>().IdStrategy(new CustomIdGeneration());
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
As of Marten 7.29.0, the event sourcing features support strong typed identifiers for the aggregated document types, but there is still no direct support for supplying strong typed identifiers for event streams yet. This may change in Marten 8.0.
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:
// 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);
}
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:
- Hand rolled types, but there's some advantage to using the next two options for JSON serialization, comparisons, and plenty of other goodness
- Vogen
- StronglyTypedId
Jumping right into an example, let's say that we want to use this identifier with Vogen for a Guid
-wrapped identifier:
[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; }
}
The usage of our Invoice
document is essentially the same as a document type with the primitive identifier types:
[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");
}
TIP
Marten 7.31.0 "fixed" it so that you don't have to use Nullable<T>
for the identity member of strong typed identifiers.
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]
- 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 sequentialGuid
support - For
int
orlong
-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:
[StronglyTypedId(Template.Int)]
public partial struct Order2Id;
public class Order2
{
public Order2Id? Id { get; set; }
public string Name { get; set; }
}
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:
[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);
}
[Fact]
public async Task load_many()
{
var issue1 = new Issue3{Name = Guid.NewGuid().ToString()};
var Issue3 = new Issue3{Name = Guid.NewGuid().ToString()};
var issue3 = new Issue3{Name = Guid.NewGuid().ToString()};
theSession.Store(issue1, Issue3, issue3);
await theSession.SaveChangesAsync();
var results = await theSession.Query<Issue3>()
.Where(x => x.Id.IsOneOf(issue1.Id, Issue3.Id, issue3.Id))
.ToListAsync();
results.Count.ShouldBe(3);
}
WARNING
LoadManyAsync()_ is not supported for strong typed identifiers
- Deleting a document by identity
- Deleting a document by the document itself
- Within
Include()
queries:
[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);
}
- 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:
[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; }
}
[ValueObject<long>]
public partial struct UpperLimit;
[ValueObject<int>]
public partial struct LowerLimit;
[ValueObject<string>]
public partial struct Description;
[ValueObject<Guid>]
public partial struct GuidId;
public class LimitedDoc
{
public Guid Id { get; set; }
public GuidId? ParentId { get; set; }
public UpperLimit? Upper { get; set; }
public LowerLimit Lower { get; set; }
public Description? Description { get; set; }
}
And the UpperLimit
and LowerLimit
value types can be registered with Marten like so:
// opts is a StoreOptions just like you'd have in
// AddMarten() calls
opts.RegisterValueType(typeof(UpperLimit));
opts.RegisterValueType(typeof(LowerLimit));
// opts is a StoreOptions just like you'd have in
// AddMarten() calls
opts.RegisterValueType(typeof(GuidId));
opts.RegisterValueType(typeof(UpperLimit));
opts.RegisterValueType(typeof(LowerLimit));
opts.RegisterValueType(typeof(Description));
And that will enable you to seamlessly use the value types in LINQ expressions like so:
[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);
}
[Fact]
public async Task store_several_and_use_in_LINQ_order_by()
{
var commonParentId = GuidId.From(Guid.NewGuid());
var doc1 = new LimitedDoc { ParentId = commonParentId, Lower = LowerLimit.From(1), Upper = UpperLimit.From(20), Description = Description.From("desc1") };
var doc2 = new LimitedDoc { Lower = LowerLimit.From(5), Upper = UpperLimit.From(25), Description = Description.From("desc3") };
var doc3 = new LimitedDoc { Lower = LowerLimit.From(4), Upper = UpperLimit.From(15), Description = Description.From("desc2") };
var doc4 = new LimitedDoc { ParentId = commonParentId, Lower = LowerLimit.From(3), Upper = UpperLimit.From(10), Description = Description.From("desc4") };
theSession.Store(doc1, doc2, doc3, doc4);
await theSession.SaveChangesAsync();
var orderedByIntBased = await theSession
.Query<LimitedDoc>()
.OrderBy(x => x.Lower)
.Select(x => x.Id)
.ToListAsync();
orderedByIntBased.ShouldHaveTheSameElementsAs(doc1.Id, doc4.Id, doc3.Id, doc2.Id);
var orderedByLongBased = await theSession
.Query<LimitedDoc>()
.OrderBy(x => x.Upper)
.Select(x => x.Id)
.ToListAsync();
orderedByLongBased.ShouldHaveTheSameElementsAs(doc4.Id, doc3.Id, doc1.Id, doc2.Id);
var orderedByStringBased = await theSession
.Query<LimitedDoc>()
.OrderBy(x => x.Description)
.Select(x => x.Id)
.ToListAsync();
orderedByStringBased.ShouldHaveTheSameElementsAs(doc1.Id, doc3.Id, doc2.Id, doc4.Id);
var orderedByGuidBased = await theSession
.Query<LimitedDoc>()
.OrderBy(x => x.ParentId)
.Select(x => x.Id)
.ToListAsync();
orderedByGuidBased.ShouldHaveTheSameElementsAs(doc1.Id, doc4.Id, doc2.Id, doc3.Id);
}