Marten V4: Hard Deletes, Soft Deletes, Un-Deletes, All the Deletes You Meet

If you haven’t seen this yet, Marten V4.0 officially dropped on Nuget late last week (finally).

V4 was a huge release for Marten and there’s lots to talk about in the new release, but I want to start simply with just talking about Marten’s support for deleting documents. In all the examples, we’re going to use a User document type from our testing code.

Let’s say we have the identity of a User document that we want to delete in our Marten database. That code is pretty simple, it’s just this below:

internal Task DeleteByDocumentId(IDocumentSession session, Guid userId)
{
    // Tell Marten the type and identity of a document to
    // delete
    session.Delete<User>(userId);

    return session.SaveChangesAsync();
}

By default, Marten will do a “hard” delete where the actual database row in Postgresql is deleted and that’s all she wrote. Marten has long had support for “soft” deletes where the underlying rows are marked as deleted with a metadata column that tracks “is deleted” instead of actually deleting the row, so let’s opt into that by configuring the User document as soft-deleted like so:

// Configuring a new Marten document store
var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");

    opts.Schema.For<User>().SoftDeleted();
});

Or if you prefer, you can also use an attribute like so:

[SoftDeleted]
public class User
{
    public Guid Id { get; set; }
    
    // Other props we don't care about here
}

And I’ll show a 3rd way that’s new in Marten V4 later on.

The API to delete a User document is exactly the same, but the mechanics of what Marten is doing to the underlying Postgresql database have changed. Instead of deleting the underlying rows, Marten just marks an mt_deleted column as true. But now, think about this Linq query below where I’m searching for all the users that have the “admin” role:

            using var session = store.QuerySession();
            var admins = await session.Query<User>()
                .Where(x => x.Roles.Contains("admin"))
                .ToListAsync();

You’ll notice that I’m not doing anything to explicitly filter out the deleted users in this query, and that’s because Marten is doing that for you — and that’s the behavior you’d want most of the time. If for some reason you’d like to query for all the documents, even the ones that are marked as deleted, you can query like this:

            var admins = await session.Query<User>()
                .Where(x => x.Roles.Contains("admin"))
                
                // This is Marten specific
                .Where(x => x.MaybeDeleted())
                .ToListAsync();

And now, if you only want to query for documents that are explicitly marked as deleted, you can do this:

            var admins = await session.Query<User>()
                .Where(x => x.Roles.Contains("admin"))
                
                // This is Marten specific
                .Where(x => x.IsDeleted())
                .ToListAsync();

So far, so good? Now what if you change your mind about your deleted documents and you want to bring them back? Marten V4 adds the IDocumentSession.UndoDeleteWhere() method to reverse the soft deletes in the database. In the usage below, we’re going to mark every admin user as “not deleted”:

            using var session = store.LightweightSession();
            
            session.UndoDeleteWhere<User>(x => x.Roles.Contains("admin"));
            await session.SaveChangesAsync();

But hold on, what if instead you really want to just wipe out the database rows with a good old fashioned “hard” delete instead? Marten V4 adds some IDocumentSession.HardDelete*****() APIs to do exactly that as shown below:

            using var session = store.LightweightSession();

            session
                .HardDeleteWhere<User>(x => x.Roles.Contains("admin") && x.IsDeleted());
            await session.SaveChangesAsync();

There are also single document versions of HardDelete() as well.

So back to the sample of querying all admin users both past and present:

            var admins = await session.Query<User>()
                .Where(x => x.Roles.Contains("admin"))

                // This is Marten specific
                .Where(x => x.MaybeDeleted())
                .ToListAsync();

If you’re going to do that, it might be nice to be able to understand which users are marked as deleted, when they were marked deleted, and which users are not deleted. And it’d also be helpful if Marten would just tag the documents themselves with that metadata.

Enter the new Marten.Metadata.ISoftDeleted interface in V4. Let’s make our User document implement that interface as shown below:

public class User : ISoftDeleted
{
    public Guid Id { get; set; }

    // These two properties reflect Marten metadata
    // and will be updated upon Delete() calls
    // or when loading through Marten
    public bool Deleted { get; set; }
    public DateTimeOffset? DeletedAt { get; set; }

    // Other props we don't care about here
}

If a document type implements the ISoftDeleted interface, the document type will automatically be treated as soft-deleted by Marten, and the Deleted and DeletedAt properties will be set at document load time by Marten.

I’m not showing it here, but you can effectively create the same behavior without the marker interface by using a fluent interface. Check out the Marten documentation for that approach.

Going into Marten I thought of the delete operation as something simple conceptually, but thousands of users means getting requests for more control over the process and that’s what we hope V4 delivers here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s