Skip to content

Audit Trails

On the Suite 3.0 ecosystem the Audit Trail feature is centralized using the AuditTrails microservice. This microservice is in charge of receiving audit information from all the other microservices through the bus, so it can store it to be queried later.

Audit Trails Client

The Audit Trails Client module provides the required components to track changes on entities so then they can be published to the Audit Trails microservice. To add the module we first need to reference the correct project adding the following to the our project file:

XML
      <ProjectReference Include="$(ServicesPath)AuditTrails/ITsynch.Suite.AuditTrails.ClientModule/ITsynch.Suite.AuditTrails.ClientModule.csproj" />

Then on our module class we add the dependency as follows:

C#
1
2
3
4
builder.DependsOn<AuditTrailsClientModule, AuditTrailsClientModuleOptions>(opts =>
{
    opts.Source = "Components";
});

All audit operations have a Source property to identify where on the ecosystem the operation was actually done. We can use the Source property on the AuditTrailsClientModuleOptions to set the source for our microservice. If it is not set it will default to AppDomain.CurrentDomain.FriendlyName.

Configuring entities

To enable Audit Trails on a given entity we need to configure it using the AuditTrailsClientModuleOptions as shown in the following example.

C#
builder.DependsOn<AuditTrailsClientModule, AuditTrailsClientModuleOptions>(opts =>
{
    opts.Source = "Components";
    opts.AddEntity<Invoice>()
    .AddProperty(r => r.CorrelationId)
    .AddProperty(r => r.DisplayName)
    .AddEntity(
        r => r.Vendor,
        o => o.ConfigureFriendlyValue(e => e?.Vendor?.DisplayName));
});

First we add the entity to be tracked using the AddEntity method. The Audit Trails Client Module uses a white listing approach for the properties, so everything is ignore by default. To add members to be tracked we can use the AddProperty, AddEntity and AddCollection methods.

The AddProperty method is used to add members that hold simple values, like int, long, string, etc.

The AddEntity method is used to add members that are objects. The method can take a second parameter that allows us to configure how the object should be tracked. The ConfigureFriendlyValue can be used to set how the friendly values should be resolved by taking a lambda of type Func<TEntity,string?>. As you can see on the example, the lambda expression for friendly values takes the reference to the entity being tracked, that way we can combine many fields on the friendly values, not just values from the object on the member. By default the AddEntity method will only track the reference to the object on the member, but not changes done to the properties of said object. If we want the changes done on the referenced object to be tracked we have to either configure it as an entity using the AddEntity method or set it as a child entity as explained in the following section.

Child Entities

By default the AddEntity method will track whether the reference to the related object changes, but it will not track the changes done to properties of that referenced object. In general each entity has its own life cycle. That being said, there are cases where an entity only exists as an extension of another "parent" entity, where their life cycles are completely intertwined. For example an Invoice entity with a DeliveryAddress sub-entity. These cases are usually mapped as One to One relationships on EF. To reflect this on the Audit Trails configuration we can use the ConfigureChild method as shown on the example below:

C#
builder.DependsOn<AuditTrailsClientModule, AuditTrailsClientModuleOptions>(opts =>
{
    opts.Source = "Test";
    opts.AddEntity<Invoice>()
    .AddProperty(r => r.CorrelationId)
    .AddProperty(r => r.DisplayName)
    .AddEntity(
        r => r.Vendor,
        o => o.ConfigureFriendlyValue(e => e?.Vendor?.DisplayName))
    .AddEntity(
        c => c.DeliveryAddress,
        o => o.ConfigureChild(m =>
        m.AddProperty(c => c.Street)
        .AddProperty(c => c.Number))
        .ConfigureFriendlyValue(
        c => c is not null
            ? $"{c.DeliveryAddress.Street}, {c.DeliveryAddress.Number}" : null));
});

When we call the ConfigureChild method we can configure the members to be tracked on the child entity. Instead of having its own configuration, the child entity configuration is bound to the parent entity.

Child Collections

A very common pattern is to have a list of children entities that depend on a parent entity, for example invoices and invoice lines, o spare requests and spare request lines. To reflect this on the Audit Trails configuration we can use the AddCollection method as shown on the example below:

C#
builder.DependsOn<AuditTrailsClientModule, AuditTrailsClientModuleOptions>(opts =>
{
    opts.Source = "Test";
    opts.AddEntity<Invoice>()
    .AddProperty(r => r.CorrelationId)
    .AddProperty(r => r.DisplayName)
    .AddEntity(
        r => r.Vendor,
        o => o.ConfigureFriendlyValue(e => e?.Vendor?.DisplayName))
    .AddEntity(
        c => c.DeliveryAddress,
        o => o.ConfigureChild(m =>
        m.AddProperty(c => c.Street)
        .AddProperty(c => c.Number))
        .ConfigureFriendlyValue(
        c => c is not null
            ? $"{c.DeliveryAddress.Street}, {c.DeliveryAddress.Number}" : null))
    .AddCollection(
        r => r.Lines,
        o => o.ConfigureChild(m =>
        {
            m.AddProperty(c => c.CorrelationId)
            .AddProperty(c => c.ProductId);
        }));
});

We have to use the ConfigureChild method to configure the members to be tracked on the child entities found in the collection. This way the audit configuration of the invoice lines is bound to the invoice. The Audit Trails client will automatically detect any elements added, removed or modified and it will mark them as creation, deletions or updates accordingly.

Disable Track

We have to use the DisableTrack method to disable the tracking for a given property. This way we will not track changes occurred in Product Id property.

C#
opts.Source = "Components";
    opts.AddEntity<Invoice>()
    .AddProperty(r => r.CorrelationId)
    .AddProperty(r => r.DisplayName)
    .AddCollection(
        r => r.Lines,
        o => o.ConfigureChild(m =>
        {
            m.AddProperty(c => c.CorrelationId)
            .AddProperty(c => c.ProductId, o => o.DisableTrack());
        }));

Hide changes

We have to use the Hide method when we want to track changes on a property but we don't want to show them in the UI. This way we will track changes occurred in Correlation Id property but we are not going to display them.

C#
opts.Source = "Components";
    opts.AddEntity<Invoice>()
    .AddProperty(r => r.CorrelationId, o => o.Hide())
    .AddProperty(r => r.DisplayName)
    .AddCollection(
        r => r.Lines,
        o => o.ConfigureChild(m =>
        {
            m.AddProperty(c => c.CorrelationId)
            .AddProperty(c => c.ProductId, o => o.DisableTrack());
        }));

Tracking changes on entities

In consumers

To track changes on entities that occurs in consumers, we have to inject the IAuditContextManager and call the BeginAuditContext method to obtain the IAuditContext instance.

Important

The IAuditContext returned implements the IAsyncDisposable so the call to BeginAuditContext should be awaited and either use a using block or a using statement.

Then we can use the returned IAuditContext to track creations, updates and deletions using the TrackCreation, TrackUpdate and TrackDeletion methods accordingly.

See the example below for more details.

C#
internal class CreateOrUpdateInvoice : IConsumer<CreateOrUpdateInvoice>
{

    private readonly IAuditContextManager auditContextManager;
    private readonly IRepository<Guid, Invoice> repository;

    public CreateOrUpdateInvoice(
    IRepository<Guid, Invoice> repository,
    IAuditContextManager auditManager)
    {
        this.repository = repository;
        this.auditManager = auditManager;
    }

    public async Task Consume(ConsumeContext<CreateOrUpdateInvoice> context)
    {
        IAuditContext auditContext;        

        var invoice = await this.repository.GetInvoiceById(context.Message.CorrelationId);

        if(invoice is null)
        {
            invoice = new Invoice(context.Message.CorrelationId);
            // ... set all properties

            await this.repository.CreateAsync(invoice);

            auditContext = this.auditContextManager.BeginAuditContext("CreateInvoice");
            auditContext.TrackCreation(invoice);
        }
        else
        {
            auditContext = this.auditContextManager.BeginAuditContext("UpdateInvoice");
            auditContext.TrackUpdate(invoice);
        }

        await using (auditContext)
        {
            /// Block of code that sets values in the properties 
            /// and publishes the Updated message.
        }
    }
}

In sagas

To track changes on entities that occurs in sagas, we have to call in our saga the BeginAuditContext method. That will set the IAuditContext instance.

See the example below for more details.

C#
using ITsynch.Suite.AuditTrails;

public class SagaInvoice : MassTransitStateMachine<Invoice>
{
    public InvoiceSaga()
        {
            InstanceState(x => x.CurrentState);

            Initially(
                When(InvoiceCreationRequested)
                    .TransitionTo(Created)
                    .BeginAuditContext("CreateInvoice")
                    // ... set properties and respond
                When(InvoiceUpdateRequested)
                    .TransitionTo(Updated)
                    .BeginAuditContext("UpdateInvoice")
                    // ... set properties and respond
                    );    
}

Then in our extension method we can do something like this:

C#
public static EventActivityBinder<Invoice, CreateInvoice> SetProperties(
            this EventActivityBinder<Invoice, CreateInvoice> binder,
            ILogger<InvoiceSaga> logger)
        {
            return binder.Then(ctx =>
                {
                    var auditContext = ctx.GetAuditContext();
                    auditContext.TrackCreation(ctx.Saga);

                    //...set properties
                }
        }

GetAuditContext method will return the IAuditContext instance that we began in the saga. With that context, we will be able to track creations, updates and deletions using the TrackCreation, TrackUpdate and TrackDeletion methods accordingly.

Add localization resource for translations

We have to use the SetLocalizationResource option method to include a localization resource of your service. There you can place some custom translations of your service.

C#
1
2
3
4
builder.DependsOn<AuditTrailsClientModule, AuditTrailsClientModuleOptions>(opts =>
{
    opts.SetLocalizationResource<AuditTrailsCustomLocalizationResources>();
});

In order to find the localization resource, you will need to add it in your ApplicationModule.

C#
1
2
3
4
5
6
builder.DependsOn<LocalizationModule, LocalizationModuleOptions>(options =>
    {
        options.Resources
            .Add<AuditTrailsCustomLocalizationResources>()
            .AddJsonEmbeddedDirectory("Resources", this.GetType().Assembly);
    });

And add this to your project's .csproj file to add all the json files in the Resources directory:

XML
1
2
3
4
5
6
    <ItemGroup>
        <Content Remove="Resources/*.json" />
    </ItemGroup>
    <ItemGroup>
        <EmbeddedResource Include="Resources/*.json" />
    </ItemGroup>

Black listing entities and properties

We can use the BlackListedEntities option to leave out entities and properties. The idea is to use the AddEntity method, which is used at compile time, to register and configure all the entities that could be audited. Then you can use the BlackListedEntities options to exclude entities and properties at runtime using any IOptions source like the Settings service or the appsettings.json file.

In the example below the DisplayName of the Invoice entity will be excluded, while the InvoiceLine will be complete excluded.

JSON
{
    "AuditTrailsClientModuleOptions": {
        "BlackListedEntities": {
            "Invoice": {
                "BlackListedProperties": ["DisplayName"]
            },
            "InvoiceLine": {
                "BlackListAllProperties": true
            }
        }
    }
}