Using sequences for unique and human-readable identifiers
This scenario demonstrates how to generate unique, human-readable (number) identifiers using Marten and PostgreSQL sequences.
Scenario
Let us assume we have a system using types with non-human-readable identifiers (e.g. Guid
) for internal system implementation. However, for end users we want to expose references to the said entities in a human-readable form. Furthermore, we need the identifiers to be unique and from a running positive sequence starting from 10000. This scenario demonstrates how to implement the described behavior using Marten and PostgreSQL sequences.
We first introduce a Marten schema customization type, deriving from FeatureSchemaBase
:
// We introduce a new feature schema, making use of Marten's schema management facilities.
public class MatterId: FeatureSchemaBase
{
private readonly int _startFrom;
private readonly string _schema;
public MatterId(StoreOptions options, int startFrom): base(nameof(MatterId), options.Advanced.Migrator)
{
_startFrom = startFrom;
_schema = options.DatabaseSchemaName;
}
protected override IEnumerable<ISchemaObject> schemaObjects()
{
// We return a sequence that starts from the value provided in the ctor
yield return new Sequence(new PostgresqlObjectName(_schema, $"mt_{nameof(MatterId).ToLowerInvariant()}"),
_startFrom);
}
}
This sequence yielding customization will be plugged into Marten via the store configuration
storeOptions.Storage.Add(new MatterId(storeOptions, 10000));
and then executed against the database (generating & executing the DDL statements that create the required database objects):
await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
We introduce a few types with Guid
identifiers, whom we reference to our end users by numbers, encapsulated in the Matter
field:
public class Contract
{
public Guid Id { get; set; }
public int Matter { get; set; }
// Other fields...
}
public class Inquiry
{
public Guid Id { get; set; }
public int Matter { get; set; }
// Other fields...
}
Now, when creating and persisting such types, we first query the database for a new and unique running number. While we generate (or if wanted, let Marten generate) non-human-readable, system-internal identifiers for the created instances, we assign to them the newly generated and unique human-readable identifier:
var matter = theStore.StorageFeatures.FindFeature(typeof(MatterId)).Objects.OfType<Sequence>().Single();
await using var session = theStore.LightweightSession();
// Generate a new, unique identifier
var nextMatter = session.NextInSequence(matter);
var contract = new Contract { Id = Guid.NewGuid(), Matter = nextMatter };
var inquiry = new Inquiry { Id = Guid.NewGuid(), Matter = nextMatter };
session.Store(contract);
session.Store(inquiry);
await session.SaveChangesAsync();
Lastly, we have an extension method (used above) as a shorthand for generating the SQL statement for a sequence value query:
public static class SessionExtensions
{
// A shorthand for generating the required SQL statement for a sequence value query
public static int NextInSequence(this IQuerySession session, Sequence sequence)
{
return session.Query<int>("select nextval(?)", sequence.Identifier.QualifiedName).First();
}
}