Practical ASP.NET
Monitor and Respond to Document Store Events in Marten
It's possible to hook into document store events to monitor (and even modify) behavior.
- By Jason Roberts
- 05/01/2017
More on This Topic:
It’s possible to hook into document store events to monitor (and even modify) behavior.
There are a number of document store events that can be monitored:
- Before changes are issued to the database
- After a change has been committed to the database
- After a document has been loaded from the database
- After a document has been explicitly added to a session
There are also asynchronous versions of some of these methods.
Creating a Custom Listener
The primary interface that’s used to create custom listener code is Marten’s IDocumentSessionListener interface, as shown in Listing 1.
Listing 1: IDocumentSessionListener interface
public interface IDocumentSessionListener
{
void BeforeSaveChanges(IDocumentSession session);
Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token);
void AfterCommit(IDocumentSession session, IChangeSet commit);
Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token);
void DocumentLoaded(object id, object document);
void DocumentAddedForStorage(object id, object document);
}
The code in Listing 2 shows a simple console application that adds a new Kingdom document, retrieves it in a different session and then deletes it.
Listing 2: Simple Console Application
using System;
using Marten;
namespace MartenConsoleExample
{
class Kingdom
{
public int Id { get; set; }
public string Name { get; set; }
public string History { get; set; }
public override string ToString()
{
return $"{Name} {History}";
}
}
class Program
{
public static DocumentStore Store { get; private set; }
static void Main(string[] args)
{
CreateDocumentStore();
int newKingdomId = InsertAKingdom();
LoadKingdom(newKingdomId);
DeleteKingdom(newKingdomId);
Console.ReadLine();
}
private static void CreateDocumentStore()
{
Store = DocumentStore.For(configure =>
{
configure.Connection(
"host = localhost; database = RPGDatabase; password = g7qo84nck22i;
username = postgres");
});
}
private static int InsertAKingdom()
{
using (IDocumentSession session = Store.LightweightSession())
{
Kingdom newKingdom = new Kingdom
{
Name = "Island of Hrothdo're",
History = "An island of mystery..."
};
session.Store(newKingdom);
session.SaveChanges();
return newKingdom.Id;
}
}
private static void LoadKingdom(int id)
{
using (IQuerySession session = Store.OpenSession())
{
session.Load<Kingdom>(id);
}
}
private static void DeleteKingdom(int id)
{
using (IDocumentSession session = Store.LightweightSession())
{
session.Delete<Kingdom>(id);
session.SaveChanges();
}
}
}
}
Rather than implement the IDocumentSessionListener interface, the helper abstract base class DocumentSessionListenerBase can be inherited from instead and selected methods overridden. Listing 3 shows a simple implementation that overrides the BeforeSaveChanges and DocumentLoaded methods.
Listing 3: CustomDocumentSessionListener
using System;
using System.Collections.Generic;
using Marten;
using Marten.Services;
namespace MartenConsoleExample
{
class CustomDocumentSessionListener : DocumentSessionListenerBase
{
public override void BeforeSaveChanges(IDocumentSession session)
{
IUnitOfWork pendingChanges = session.PendingChanges;
IEnumerable<Kingdom> pendingKingdomInserts = pendingChanges.InsertsFor<Kingdom>();
IEnumerable<Kingdom> pendingKingdomUpdates = pendingChanges.UpdatesFor<Kingdom>();
IEnumerable<IDeletion> pendingKingdomDeletes = pendingChanges.DeletionsFor<Kingdom>();
Console.WriteLine("Pending inserts:");
foreach (Kingdom kingdom in pendingKingdomInserts)
{
Console.WriteLine($" - {kingdom}");
}
Console.WriteLine("Pending updates:");
foreach (Kingdom kingdom in pendingKingdomUpdates)
{
Console.WriteLine($" - {kingdom}");
}
Console.WriteLine("Pending deletes:");
foreach (IDeletion kingdom in pendingKingdomDeletes)
{
Console.WriteLine($" - {kingdom}");
}
}
public override void DocumentLoaded(object id, object document)
{
Console.WriteLine($"Document loaded: {document}");
}
}
}
Configuring Document Session Listeners
To actually make use of the CustomDocumentSessionListener it must be configured in the document store. The following code shows a modified CreateDocumentStore method that adds a new instance of the custom listener:
private static void CreateDocumentStore()
{
Store = DocumentStore.For(configure =>
{
configure.Connection(
"host = localhost; database = RPGDatabase; password = g7qo84nck22i;
username = postgres");
// Add listener to document store - can add multiple listeners if required
configure.Listeners.Add(new CustomDocumentSessionListener());
});
}
When the application is executed now, the custom code in the BeforeSaveChanges and DocumentLoaded methods will be executed as the following console output demonstrates:
Pending inserts:
- Island of Hrothdo're An island of mystery ...
Pending updates:
Pending deletes:
Document loaded: Island of Hrothdo're An island of mystery ...
Pending inserts:
Pending updates:
Pending deletes:
- Delete MartenConsoleExample.Kingdom with Id 35001: delete from public.mt_doc_kingdom where id = ?
To learn more about Marten, check out the documentation and the series of articles on my Don’t Code Tired blog.