In the past, I’ve spoken about using Servicestack with Raven DB for building REST APIs. Inevitably, we get to a point where we need audit trails for the API. Audit trails are different from Request logs in that you want to capture not just the request but the details of the actual changes resulting from the request. Since I’m not a big fan of re-inventing the wheel, I used the Audt.net framework since it meets my needs.

However, it currently does not have a RavenDB data provider. So I wrote one that supports Raven DB 4.x. I’ve tested it on RavenDB 4.2.3. I’ve provided the code below and intend to send a PR to the project with the provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using System.Threading.Tasks;
using Audit.Core;
using Raven.Client.Documents;
using ServiceStack;

/// <summary>
/// Defines a data provider to allow storage of <see cref="AuditEvent"/>
/// in raven db.
/// </summary>
public class RavenDataProvider : AuditDataProvider
{
    /// <summary>
    /// An instance of the <see cref="IDocumentStore"/> to be used for RavenDB
    /// operations.
    /// </summary>
    private readonly IDocumentStore documentStore;

    /// <summary>
    /// Initializes a new instance of the <see cref="RavenDataProvider"/> class.
    /// </summary>
    /// <param name="store">
    /// The document store to be used.
    /// </param>
    public RavenDataProvider(IDocumentStore store)
    {
        this.documentStore = store;
    }

    /// <inheritdoc cref="AuditDataProvider.InsertEvent"/>
    public override object InsertEvent(AuditEvent auditEvent)
    {
        using var session = this.documentStore.OpenSession();
        session.Store(auditEvent);
        session.SaveChanges();
        return auditEvent;
    }

    /// <inheritdoc cref="AuditDataProvider.ReplaceEvent"/>
    public override void ReplaceEvent(object eventId, AuditEvent auditEvent)
    {
        using var session = this.documentStore.OpenSession();
        var retrievedEvent = session.Load<AuditEvent>((string)eventId);
        retrievedEvent.PopulateWithNonDefaultValues(auditEvent);
        session.Store(retrievedEvent);
        session.SaveChanges();
    }

    /// <inheritdoc cref="AuditDataProvider.GetEvent{T}(object)"/>
    public override T GetEvent<T>(object eventId)
    {
        using var session = this.documentStore.OpenSession();
        var retrievedEvent = session.Load<AuditEvent>((string)eventId);
        return AuditEvent.FromJson<T>(retrievedEvent.ToJson());
    }

    /// <inheritdoc cref="AuditDataProvider.InsertEventAsync(AuditEvent)"/>
    public override async Task<object> InsertEventAsync(AuditEvent auditEvent)
    {
        using var session = this.documentStore.OpenAsyncSession();
        await session.StoreAsync(auditEvent);
        await session.SaveChangesAsync();
        return auditEvent;
    }

    /// <inheritdoc cref="AuditDataProvider.ReplaceEventAsync(object, AuditEvent)"/>
    public override async Task ReplaceEventAsync(object eventId, AuditEvent auditEvent)
    {
        using var session = this.documentStore.OpenAsyncSession();
        var retrievedEvent = await session.LoadAsync<AuditEvent>((string)eventId);
        retrievedEvent.PopulateWithNonDefaultValues(auditEvent);
        await session.StoreAsync(retrievedEvent);
        await session.SaveChangesAsync();
    }

    /// <inheritdoc cref="AuditDataProvider.GetEventAsync{T}(object)"/>
    public override async Task<T> GetEventAsync<T>(object eventId)
    {
        using var session = this.documentStore.OpenAsyncSession();
        var retrievedEvent = await session.LoadAsync<AuditEvent>((string)eventId);
        return AuditEvent.FromJson<T>(retrievedEvent.ToJson());
    }
}

Now the default audit event is missing the ‘ID’ member needed by RavenDB, and it’s missing some other things that we want. Since our audit events are going to live in a multi-tenant environment, we need the id of the tenant and the actor. We don’t want to use CustomFields because they’re a pain to index, and we don’t want to bother having to query on them. Also, we want to define the diff between the old and the new, so the UI developers have an easy time displaying what changed to the end-user.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Audit.Core;

/// <summary>
/// Defines a model for audit events within the application.
/// </summary>
public class AppAuditEvent : AuditEvent
{
    /// <summary>
    /// Gets or sets the id of the event.
    /// </summary>
    /// <remarks>
    /// Added for raven db.
    /// </remarks>
    public string Id { get; set; }

    /// <summary>
    /// Gets or sets the id of the user performing the action.
    /// </summary>
    public string ActorId { get; set; }

    /// <summary>
    /// Gets or sets the id of the tenant within which the action is being performed.
    /// </summary>
    public string InstitutionId { get; set; }

    /// <summary>
    /// Gets or sets the details of the change made as part of the audit event.
    /// </summary>
    public string Diff { get; set; }
}

Let’s talk about diffing the objects for a second. There are many ways to do it, but the best is to use RFC 6902 because it gives us the best display for changes. Again, Nuget came in quite handy, and I found the library: JsonDiffPatch.

Great. Looks good so far. Now, we want to make this as foolproof as possible to perform auditing. Which means that we have to have it all set up using the Servicestack DI container. So let’s set up the necessary items within the startup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ravenDataProvider = new RavenDataProvider(RavenExtensions.GetStore(this.apiSettings));
Audit.Core.Configuration.DataProvider = ravenDataProvider;
Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
{
    if ((AppAuditEvent)scope.Event == null)
    {
        return;
    }

    var actualEvent = (AppAuditEvent)scope.Event;
    actualEvent.Diff = new JsonDiffer().Diff(JToken.Parse(actualEvent.Target.SerializedOld.ToString()), JToken.Parse(actualEvent.Target.SerializedNew.ToString()), true).ToString();
});

At this point, we’ve set up just about everything except for the boilerplate that hydrates the event with the id of the Actor, Institution, and any other items we might want to add in the future. And once again, the most critical part is to make sure that the code is maintainable. So, I updated the filter from my previous post with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
this.RegisterTypedRequestFilter<IServiceModel>((req, res, dtoInterface) =>
{
    dtoInterface.Id = dtoInterface.Id.IsNullOrEmpty() ? dtoInterface.Id : dtoInterface.GetFullyQualifiedRavenId();

    var session = req.GetSession();
    var dbSession = HostContext.Resolve<IDocumentSession>();
    var authenticatedUser = new AuthenticatedUser(session, dbSession);

    var auditOptions = new AuditScopeOptions { AuditEvent = new AppAuditEvent { InstitutionId = authenticatedUser.GetAccessibleInstitution(), ActorId = authenticatedUser.GetUserId() }, EventType = $"{req.Verb}:{req.OperationName}" };
    var scope = AuditScope.Create(auditOptions);

    HostContext.Container.Register(c => scope).ReusedWithin(ReuseScope.Request);
});

Since all POST, PUT, DELETE, GET operations implement the IServiceModel interface, all the details required for the audit trail are present for consumption in any service, by requesting the audit scope as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class UserService : Service
{
    /// <summary>
    /// Gets or sets the document session to be used when performing database operations.
    /// </summary>
    public IDocumentSession Session { get; set; }

    /// <summary>
    /// Gets or sets the audit scope.
    /// </summary>
    public AuditScope AuditScope { get; set; }

    /// <summary>
    /// Gets or sets the API settings.
    /// </summary>
    public ApiSettings ApiSettings { get; set; }

And now, we can audit trail with a single line, as shown below for the PUT call for our UserService.

1
2
3
4
5
6
public UserDetail Put(User request)
{
    var authenticatedUser = new AuthenticatedUser(this.GetSession(), this.Session);

    var user = this.Session.Load<ServiceModel.DataObjects.User>(request.Id);
    this.AuditScope.SetTargetGetter(() => user);

DI takes care of everything since the actual audit event is saved on disposal of the audit scope instance, which happens at the end of the request, as defined by us within the container registration above. The upside of all this is that we have the duration of the event, as well. And here’s what the event looks like in our Raven DB instance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
    "EventType": "PUT:User",
    "ActorId": "users/2-A",
    "InstitutionId": "institutions/1-A",
    "Diff": "[\r\n  {\r\n    \"op\": \"replace\",\r\n    \"path\": \"/FirstName\",\r\n    \"value\": \"Test23\"\r\n  },\r\n  {\r\n    \"op\": \"replace\",\r\n    \"path\": \"/LastName\",\r\n    \"value\": \"audit23\"\r\n  }\r\n]",
    "Environment": {
        "UserName": "Arun Pereira",
        "MachineName": "HOME",
        "DomainName": "HOME",
        "CallingMethodName": "App.AppHost+<>c.<ConfigureFilters>b__4_2()",
        "AssemblyName": "App, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
        "Culture": "en-CA"
    },
    "Target": {
        "Type": "User",
        "Old": {
            "Id": "users/2-A",
            "FirstName": "Test2",
            "LastName": "audit2",
            "EmailAddress": "tstadmin@tst.ca",
            "Password": "AQAAAAEAACcQAAAAEAYCOYies9W4SZw9vE3vJ5l0e4SR+/Mw9Kx47yc0WQPrkzZUzfWu22d4xzEa8rHucA==",
            "InstitutionId": "institutions/1-A",
            "Roles": [
                2
            ],
            "CreatorId": "users/1-A",
            "IsRegistrationCompleted": true,
            "InstitutionUserId": "S01",
            "IsActive": true,
            "IsArchived": false,
            "ContactNumbers": [
                {
                    "Type": 3,
                    "Number": "9090909090",
                    "Extension": ""
                }
            ]
        },
        "New": {
            "Id": "users/2-A",
            "FirstName": "Test23",
            "LastName": "audit23",
            "EmailAddress": "tstadmin@tst.ca",
            "Password": "AQAAAAEAACcQAAAAEAYCOYies9W4SZw9vE3vJ5l0e4SR+/Mw9Kx47yc0WQPrkzZUzfWu22d4xzEa8rHucA==",
            "InstitutionId": "institutions/1-A",
            "Roles": [
                2
            ],
            "CreatorId": "users/1-A",
            "IsRegistrationCompleted": true,
            "InstitutionUserId": "S01",
            "IsActive": true,
            "IsArchived": false,
            "ContactNumbers": [
                {
                    "Type": 3,
                    "Number": "9090909090",
                    "Extension": ""
                }
            ]
        }
    },
    "StartDate": "2019-10-16T01:13:05.0803954",
    "EndDate": "2019-10-16T01:13:05.4670120",
    "Duration": 387,
    "@metadata": {
        "@collection": "AppAuditEvents",
        "Raven-Clr-Type": "App.ServiceInterface.AppAuditEvent, App.ServiceInterface"
    }
}

And we’re done. We can now perform audit generation from anywhere within any of our services.