March 7, 2012

Using a RavenDB listener to update entities

If you’ve used NHibernate you’ll be familiar with the concept of listeners - hooks into the NHibernate Event System.

It’s no surprise then that RavenDB also supports event listeners, with a much simpler API (in my opinion).

The following listener interfaces exist in the Raven.Client.Listeners namespace:

  • IDocumentConversionListener - Hook for users to provide additional logic for converting to / from entities to document / metadata pairs.
  • IDocumentDeleteListener - Hook for users to provide additional logic on Delete operations.
  • IDocumentQueryListener - Hook for users to modify all queries globally.
  • IDocumentStoreListener - Hook for users to provide additional logic on Store operations.

In this post we’ll look at using a IDocumentStoreListener to update some fields on an entity before it is persisted to the database.

This example is fairly common. You want to record the time an object was changed and the name of the user that made that change. Of course you could do this manually on each entity type but where’s the fun in that?

First we define a base class for entities that we wish to “audit” (see note at the end of this post regarding proper auditing):

public abstract class AuditableEntity
{
    public string UpdatedBy { get; set; }
    public DateTime UpdatedOn { get; set; }
}

And an entity that inherits the above:

public class Article : AuditableEntity
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
}

Now we need to create a listener to update the audit fields:

public class AuditableEntityListener : IDocumentStoreListener
{
    private Func<string> getCurrentUser;

    public AuditableEntityListener(Func<string> getCurrentuser)
    {
        this.getCurrentUser = getCurrentuser;
    }

    public void AfterStore(string key, object entityInstance, RavenJObject metadata)
    {

    }

    public bool BeforeStore(string key, object entityInstance, RavenJObject metadata)
    {
        var auditableEntity = entityInstance as AuditableEntity;

        if (auditableEntity == null)
            return false;

        auditableEntity.UpdatedBy = getCurrentUser();
        auditableEntity.UpdatedOn = DateTime.UtcNow;

        return true;
    }
}

Note that we’re passing in a delegate function to get the current user’s name. Listeners are singleton so this is the best way to access contextual information.

In the BeforeStore method we check to see if the entity inherits AuditableEntity. If so, we update the audit fields and return true. This tells Raven that we have modified the entity so it needs to be reserialized.

To register a listener use the DocumentStore.RegisterListener method:

store = new EmbeddableDocumentStore
{
    RunInMemory = true
}
.RegisterListener(new AuditableEntityListener(() => "Test User"))
.Initialize();

In a web application we would probably have something like:

private string GetCurrentUser()
{
    if (HttpContext.Current != null 
        && HttpContext.Current.User != null)
    {
        return HttpContext.Current.User.Identity.Name;
    }

    return string.Empty;
}

//

store = new DocumentStore
{
    ConnectionStringName = "RavenDb"
}
.RegisterListener(new AuditableEntityListener(GetCurrentUser))
.Initialize();

We can verify that the listener is working with the following test:

[TestFixture]
public class ListenerTests : RavenTest
{
    [Test]
    public void WhenEntityIsStored_AuditFieldsAreUpdated()
    {           
        var article = new Article { 
            Title = "Test Article", 
            Content = "Test Article Content" 
        };

        using (var session = Store.OpenSession())
        {
            session.Store(article);
            session.SaveChanges();

            Assert.That(
                Math.Abs((DateTime.UtcNow - article.UpdatedOn).TotalMinutes) < 1);
            Assert.That(
                article.UpdatedBy == "Test User");
        }
    }
}

public class RavenTest
{
    private IDocumentStore store;
    public IDocumentStore Store { get { return store; } }

    [SetUp]
    public void CreateStore()
    {
        store = new EmbeddableDocumentStore
        {
            RunInMemory = true
        }
        .RegisterListener(new AuditableEntityListener(() => "Test User"))
        .Initialize();
    }

    [TearDown]
    public void DestroyStore()
    {
        store.Dispose();
    }
}

This example demonstrates how to update fields on an entity using a RavenDB listener. It is not a good example of how to implement auditing in your application since it does not tell us what has changed. For that you should look at the RavenDB versioning bundle.

© 2022 Ben Foster