I’d like to introduce another value-added AutoCrud feature that I’m pretty excited about and happy to hear any feedback on missing functionality from anyone else who’s interested in using this feature, the best name of which I can come up with is “Executable Crud Audit Events”.
Quick Event Sourcing Overview
A little background first, Event Sourcing is a fairly popular approach for designing systems where instead of modifying normalized tables directly, all mutations would be captured as an “Event” that would be written to an “Event Store” who’d send it to its consumers who would act on those events, to, for example write to a materialized view that’s optimized to serve its read only system queries. Some differences with normal RDBMS development is that the Event Store is the master authority of your Data which would let you rebuild your RDBMS from scratch by replaying all events through your system and have it re-create your materialized views.
But I’ve never considered using ES due to the additional complexity it adds to a system, e.g. requires more infrastructure and moving parts, more dev overhead & additional artifacts, hurts productivity as system features would need to be routed through your systems ES funnel, can no longer rely on ACID properties of having an “always correct RDBMS state” instead you’d need to design system features to account for Eventual Consistency.
But being able to rebuild your Systems DB by replaying all system events is pretty a nice property where you can more easily redesign your RDBMS to more easily remove legacy schema designs and allow you to optimize your RDBMS design for querying, it also makes it easier to move to a NoSQL DB as being able to publish denormalized data lessens the reliance on needing RDBMS’s relational features.
As data is the most important part of most systems I prefer to keep a lot of Audit history, e.g. when items were created, modified and deleted (and by whom). Typically this means I’ll use “Soft Delete” and “Append Only” approaches to be able to maintain a history of changes to important data.
Executable Crud Audit Events
This feature tries to obtain some of the nice features of ES but without the additional complexity by allowing you to capture all CRUD operations in an executable log whilst still retaining your RDBMS as your master authority. This feature doesn’t require any additional dev overhead as your AutoCrud Request DTOs are the recorded events.
To enable this feature you just need to register an ICrudEvents provider which will let you persist your events in any data store, but typically you’d use OrmLiteCrudEvents
to persist it in the same RDBMS that the AutoCrud requests are already writing to, e.g:
container.AddSingleton<ICrudEvents>(c =>
new OrmLiteCrudEvents(c.Resolve<IDbConnectionFactory>()) {
// NamedConnections = { SystemDatabases.Reporting }
});
container.Resolve<ICrudEvents>().InitSchema();
If you’re using Multitenancy features or multiple RDBMS’s in your AutoCrud DTOs you can add them to NamedConnections
where it will create an CrudEvent
table in each of the RDBMS’s used.
and that’s all that’s required, now every AutoCrud operation will persist the Request DTO and associative metadata in the Event entry below within a DB transaction:
public class CrudEvent : IMeta
{
[AutoIncrement]
public long Id { get; set; }
// AutoCrudOperation, e.g. Create, Update, Patch, Delete, Save
public string EventType { get; set; }
public string Model { get; set; } // DB Model Name
public string ModelId { get; set; } // Primary Key of DB Model
public DateTime EventDate { get; set; } // UTC
public long? RowsUpdated { get; set; } // How many rows were affected
public string RequestType { get; set; } // Request DTO Type
public string RequestBody { get; set; } // Serialized Request Body
public string UserAuthId { get; set; } // UserAuthId if Authenticated
public string UserAuthName { get; set; } // UserName or unique User Identity
public string RemoteIp { get; set; } // Remote IP of the Request
public string Urn { get; set; } // URN format: urn:{requesttype}:{ModelId}
// Custom Reference Data with or with non-integer Primary Key
public int? RefId { get; set; }
public string RefIdStr { get; set; }
public Dictionary<string, string> Meta { get; set; }
}
Full Executable Audit History
With what’s captured this will serve as an Audit History of state changes for any row by querying the Model
& ModelId
columns, e.g:
var dbEvents = (OrmLiteCrudEvents)container.Resolve<ICrudEvents>();
var rowAuditEvents = dbEvents.GetEvents(Db, nameof(Rockstar), id);
The contents of the Request DTO stored as JSON in RequestBody
. You can quickly display the contents of any JSON in human-friendly HTML with the htmlDump script if you’re using #Script
, @Html.HtmlDump(obj)
if you’re using Razor or just the static ViewUtils.HtmlDump(obj)
method to get a raw pretty-formatted HTML String.
Replay AutoCrud Requests
If all your database was created with AutoCrud Services you could delete its rows and re-create it by just re-playing all your AutoCrud DTOs in the order they were executed, which can be done with:
var eventsPlayer = new CrudEventsExecutor(appHost);
foreach (var crudEvent in dbEvents.GetEvents(db))
{
await eventsPlayer.ExecuteAsync(crudEvent);
}
The CrudEventsExecutor
uses your AppHost’s ServiceController
to execute the message, e,g. same execution pipeline MQ Requests use, so it will execute your AppHost’s GlobalMessageRequestFilters/Async
if you have any custom logic in Request Filters (e.g. Multi TenantId example above). It also executes authenticated AutoCrud requests as the original AutoCrud Request Authenticated User, which just like JWT Refresh Tokens
will require either using an AuthRepository or if you’re using a Custom Auth Provider you can implement an IUserSessionSource
to load User Sessions from a custom data store.
When replaying the Audit Events it will use the original primary key, even if you’re using [AutoIncrement]
Primary Keys, this will let you re-create the state of a single entry, e.g:
db.DeleteById<Rockstar>(id);
var rowAuditEvents = dbEvents.GetEvents(Db, nameof(Rockstar), id);
foreach (var crudEvent in rowAuditEvents)
{
await eventsPlayer.ExecuteAsync(crudEvent);
}
If for instance you wanted it to execute through your latest logic with any enhancements or bug fixes, etc.
This feature is now available in the latest v5.8.1 on MyGet if you want to try it out early, feedback welcome!