Damian Hickey

Mostly software and .NET related. Mostly.

How we are doing Domain Event versioning

(x/post and edited from the DDD/CQRS group)

Edit: Some background info... A Domain Event is expected to be around forever. This means that we need to be able to read and deserialize events that could be 5, 7 or a decade old. Think of them as little file schemas - we need to keep the knowledge of that schema and keep it intact the lifetime our the data, which may be longer than the application itself.

So, I think this will work for BEAM for and our particular requirements - I'm certainly not prescribing this as 'best practice' or anything. I've decided to have two representations of a Domain Event, which I have called "Application Domain Event" and the other "Storage Domain Event".

Application Domain Event:

  1. Is published on bus.
  2. Can be referenced and depended on by any part of the application / system.
  3. Can follow OO patterns, such as DRY, i.e. sharing a type / enum with a command in a common location is permitted.
  4. Is versioned is the standard .net application / assembly manner. Old versions are not kept around.

Storage Domain Event:

  1. Is the schema format for serializing to, and deserializing from, the event store.
  2. Is in a project that has no external dependencies, except for 'System'. (And maybe System.Runtime.Serialization if they need to be marked [DataContract] etc).
  3. Is not referenced or consumed by any part of the system / application except for the event store.
  4. Versioning is done for the entire set of domain events, rather than individual. Currently this is organised by namespace, but may do seperate assemblies later.
  5. Will be kept around 'forever'.
  6. Will, on occasion, utilize wholesale migration if it makes sense.

I use Jonathan Oliver's event store library and am utilizing pipeline hooks to convert between Storage and Application events when read (up conversion), when committed (commit conversion) and published (an IDispatchCommits wrapper). These converters are in a separate library and are all, at this point, AutoMapper based with minimal configuration. There is now an additional developer cost in creating / managing the storage representation as well as the application one, but it doesn't seem to be completely wild (yet). A test using AutoMapper's AssertConfigurationIsValid catches most event shape mismatch issues very quickly. There is also a perf hit in the mapping but my gut tells me it's small compared to serialization and event store I/O.

For 'locking' a version of the storage events I have a glorified copy -> fine/replace script+tool (me-ware) that duplicates a storage version, changes the namespace (i.e. ".V1." -> ".V2."), generates a type signature, creates a test to make sure that types don't change and updates converters. I'm not convinced that this is ultimately necessary or worth it. I have some time to chew on that and am in no particular rush to settle on an approach.

So, will this stand the test of time? Time will tell I suppose :)

RavenDB Revisions Bundle

While Raven's 'Versioning' bundle is designed for regulatad environments, such as healtcare, where nothing can be deleted, this bundle is more useful for applications where the document is a result of a projection (i.e. CQRS), where the application controls the document version numbers. It differs in the following ways:

    It only works on documents that have a Revision property.
    It allows Revisions to be deleted.
    It allows Revisions to be overwritten.
    Your application controls the Revision number and not the plugin.
    Your application is resposible for revision number contiguity.


Install

Packages are available nuget.org (source on github):

Using

If you want your this plugin to automatically create revision copies add a Revision property to your document:

public class MyDocument
{
    public string Id { get; set;}
   
    public int Revision { get; set; }
}

When a revisionable document is saved, the plugin will create a copy with a new id. These copies are excluded from all indexes. You should be aware that these revisions are kept indefinitely until you have specifically deleted them or used Raven's 'Expiration' bundle to perform automatic purging.

To retireve a specific revision of a document use the LoadRevision extension method:

using(var session = store.OpenSession)
{
    var doc = session.LoadRevision("key", 2);
}

To delete a specific revision of a document:

_documentStore.DatabaseCommands.DeleteRevision("key", 2, null);

 

RavenDB Client Contextual Listeners

In a previous post, I described how to get contextual information into a RavenDB client listener. I've since create a NuGet package (source) to make things a little easier.

To recap, listeners allow you to hook into all document operations that occur in each session. The current design of the listeners is that their lifecyle is singleton with respect to the document database, making it a little tricky to pass in contextual information.

There are 4 listeners interfaces define in Raven.Abstractions that allow you to intercept the data sent to, or recieved from, a Raven database:

    IDocumentStoreListener
    IDocumentDeleteListener
    IDocumentConversionListener
    IDocumentQueryListener

So correspondingly, there are 4 context aware listeners that you can register:

    ContextualDocumentStoreListener<>
    ContextualDocumentDeleteListener<>
    ContextualDocumentConversionListener<>
    ContextualDocumentQueryListener<>

... and there are 4 base listener context types that you inherit from that perform the contextual work:

    AbstractDocumentStoreListenerContext
    AbstractDocumentDeleteListenerContext
    AbstractDocumentConversionListenerContext
    AbstractDocumentQueryListenerContext

An example is the best way to demonstrate how to use these.

Example

Say you want to add the current username to the metadata of each document that is stored. Therefore we want an IDocumentStoreListener to intercept each document before storing and add our username metadata.

First define your username document store context type:

public class UserNameContext : AbstractDocumentStoreListenerContext
{
    private readonly string _userName;

    public UserNameContext(string userName)
    {
        _userName = userName;
    }

    protected override void AfterStore(string key, object entityInstance, RavenJObject metadata)
    {}

    protected override bool BeforeStore(string key, object entityInstance, RavenJObject metadata)
    {
        metadata.Add("UserName", RavenJToken.FromObject(_userName));
        return false; //return true if you modify the entityInstance
    }
}

Register your the listener that will be aware of the UserNameContext:

var documentStore = new DocumentStore()
{
    Url = "http://server"
};

// can be called after .Initialize(), doesn't matter
documentStore.RegisterListener(new ContextualDocumentStoreListener<UserNameContext>());
documentStore.Initialize();

And the sweet bit.. just open a new context before you open a session:

using(new UserNameContext("The UserName"))
using(var session = documentStore.OpenSession())
{
    session.Store(new Doc());
    session.SaveChanges();
}