Sneak Peek of Strong Typed Identifiers in Marten

If you really need to have strong typed identifier support in Marten right now, here’s the long standing workaround.

Some kind of support for “strong typed identifiers” has long been a feature request for Marten from our community. I’ve even been told by a few folks that they wouldn’t consider using Marten until it did have this support. I’ve admittedly been resistant to adding this feature strictly out of (a very well founded) fear that tackling that would be a massive time sink that didn’t really improve the tool in any great way (I’m hoping to be wrong about that).

My reticence about this aside, it came up a couple times in the past week from JasperFx Software customers, and that magically ratchets up the priority quite a bit. That all being said, here’s a little preview of some ongoing work for the next Marten feature release.

Let’s say that you’re using the Vogen library for value types and want to use this custom type for the identity of an Invoice document in Marten:

[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; }
}

Jumping to some already passing tests, Marten can assign an identity to a new document is one is missing just like it would today for Guid identities:


    [Fact]
    public void store_document_will_assign_the_identity()
    {
        var invoice = new Invoice();
        theSession.Store(invoice);

        // Marten sees that there is no existing identity,
        // so it assigns a new identity 
        invoice.Id.ShouldNotBeNull();
        invoice.Id.Value.Value.ShouldNotBe(Guid.Empty);
    }

Because this actually does matter for database performance, Marten is using a sequential Guid inside of the custom InvoiceId type. Following Marten’s desire for a “it just works” development experience, Marten is able to “know” how to work with the InvoiceId type generated by Vogen without having to require any kind of explicit mapping or mandatory interfaces on the identity type — which I thought was pretty important to keep your domain code from being coupled to Marten.

Moving to basic use cases, here’s a passing test for storing and loading a new document from the database:

    [Fact]
    public async Task load_document()
    {
        var invoice = new Invoice{Name = Guid.NewGuid().ToString()};
        theSession.Store(invoice);

        await theSession.SaveChangesAsync();

        (await theSession.LoadAsync<Invoice>(invoice.Id))
            .Name.ShouldBe(invoice.Name);
    }

and a look at how the strong typed identifiers can play in LINQ expressions so far:

    [Fact]
    public async Task use_in_LINQ_where_clause()
    {
        var invoice = new Invoice{Name = Guid.NewGuid().ToString()};
        theSession.Store(invoice);

        await theSession.SaveChangesAsync();

        var loaded = await theSession.Query<Invoice>().FirstOrDefaultAsync(x => x.Id == invoice.Id);

        loaded
            .Name.ShouldBe(invoice.Name);
    }

    [Fact]
    public async Task load_many()
    {
        var invoice1 = new Invoice{Name = Guid.NewGuid().ToString()};
        var invoice2 = new Invoice{Name = Guid.NewGuid().ToString()};
        var invoice3 = new Invoice{Name = Guid.NewGuid().ToString()};
        theSession.Store(invoice1, invoice2, invoice3);

        await theSession.SaveChangesAsync();

        var results = await theSession
            .Query<Invoice>()
            .Where(x => x.Id.IsOneOf(invoice1.Id, invoice2.Id, invoice3.Id))
            .ToListAsync();
        
        results.Count.ShouldBe(3);
    }

    [Fact]
    public async Task use_in_LINQ_order_clause()
    {
        var invoice = new Invoice{Name = Guid.NewGuid().ToString()};
        theSession.Store(invoice);

        await theSession.SaveChangesAsync();

        var loaded = await theSession.Query<Invoice>().OrderBy(x => x.Id).Take(3).ToListAsync();
    }

There’s a world of use case permutations yet to go (bulk writing, numeric identities with HiLo generation, Include() queries, more LINQ scenarios, magically adding JSON serialization converters, using StrongTypedId as well), but I think we’ve got a solid start on a long asked for feature that I’ve previously been leery of building out.

Leave a comment