Application triggers for Asp.Net Core 2.1 entity framework core

Hello and welcome 🙂

Today I wanted to talk about extending your application and your DbContext to run arbitrary code when a save occurs.

The backstory

While working with quite a few applications that work with databases especially using entity framework, I noticed the pattern of saving changes to the database and then do something else based on those changes. A few examples of that are as follows:

  • When the user state changes, reflect that in the UI.
  • When adding or updating a product, update the stock.
  • When deleting an entity then do another action like check for validity.
  • When an entity changes in any way (add, update, delete), send that out to an external service.

These are mostly akin to having database triggers when the data changes, some action needs to be performed, but those actions are not always database related, more as a response to the change in the database, which sometimes it is just business logic.

As such, in one of these applications, I found a way to incorporate that behavior and clean up the repetitive code that would follow, while also keeping it maintainable by just registering the triggers into the IoC container of Asp.Net core.

In this post we will be having a look at the following:

  • How to extend the DbContext to allow for the triggers.
  • How to register multiple instances into the container using the same interface or base class.
  • How to create entity instances from tracked changes so we can work with concrete items.
  • How to limit our triggers to only fire under certain data conditions.
  • Injecting dependencies into our triggers.
  • Avoiding infinite loops in our triggers.

We have a long enough road ahead to let’s get started.

Creating the triggers framework

ITrigger interface

We will start off with the root of our triggers and that is the ITrigger interface.

using Microsoft.EntityFrameworkCore.ChangeTracking;

public interface ITrigger
{
    void RegisterChangedEntities(ChangeTracker changeTracker);
    Task TriggerAsync();
}
  • The RegisterChangedEntities method accepts a ChangeTracker so that if need be, we can store the changes that happened for later use.
  • The TriggerAync method actually runs our logic, the reason why these two are separate we will see when we will do the changes to the DbContext.

TriggerBase base class

Next, off we will be looking at a base class that is not mandatory though it does exist for two main reasons:

  1. To house the common logic of the triggers, including the state of the tracked entities.
  2. To be able to filter out trigger based on the entity they are meant for.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.ChangeTracking;

public abstract class TriggerBase<T> : ITrigger
{
    protected IEnumerable<TriggerEntityVersion<T>> TrackedEntities;

    protected abstract IEnumerable<TriggerEntityVersion<T>> RegisterChangedEntitiesInternal(ChangeTracker changeTracker);

    protected abstract Task TriggerAsyncInternal(TriggerEntityVersion<T> trackedTriggerEntity);

    public void RegisterChangedEntities(ChangeTracker changeTracker)
    {
        TrackedEntities = RegisterChangedEntitiesInternal(changeTracker).ToArray();
    }

    public async Task TriggerAsync()
    {
        foreach (TriggerEntityVersion<T> triggerEntityVersion in TrackedEntities)
        {
            await TriggerAsyncInternal(triggerEntityVersion);
        }
    }
}

Let’s break it down member by member and understand what’s with this base class:

  1. The class is a generic type of T, this ensures that the logic that will be running in any of its descendants will only apply to a specific entity that we want to run our trigger against.
  2. The protected TrackedEntities field holds on to the changed entities, both before and after the change so we can run our trigger logic against them.
  3. The abstract method RegisterChangedEntitiesInternal will be overridden in concrete implementations of this class and ensures that give a ChangeTracker it will return a set of entities we want to works against. This is not to say that it cannot return an empty collection, it’s just that if we opt to implement a trigger via the TriggerBase class, then it’s highly likely we would want to hold onto those instances for later use.
  4. The abstract method TriggerAsyncInternal runs our trigger logic against n entity we saved from the collection.
  5. The public method RegisterChangedEntities ensures that the abstract method RegisterChangedEntitiesInternal is called, then it calls .ToArray() to ensure that if we have an IEnumerable query, that it also actually executes so that we don’t end up with a collection that is updated later on in the process in an invalid state. This is mostly a judgment call on my end because it is easy to forget that IEnumerable queries have a deferred execution mechanic.
  6. The public method TriggerAsync just enumerates over all of the entities calling TriggerAsyncInternal on each one.

Now that we discussed the base class, it’s time we move on to the definition of a TriggerEntityVersion

The TriggerEntityVersion class

The TriggerEntityVersion class is a helper class that serves the purpose of housing the old and the new instance of a given entity.

using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore.ChangeTracking;

public class TriggerEntityVersion<T>
{
    public T Old { get; set; }
    public T New { get; set; }

    public static TriggerEntityVersion<TResult> CreateFromEntityProperty<TResult>(EntityEntry<TResult> entry) where TResult : class, new()
    {
        TriggerEntityVersion<TResult> returnedResult = new TriggerEntityVersion<TResult>
        {
            New = new TResult(),
            Old = new TResult()
        };

        foreach (PropertyInfo propertyInfo in typeof(TResult)
                                                 .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
                                                 .Where(pi => entry.OriginalValues.Properties.Any(property => property.Name == pi.Name)))
        {
            if (propertyInfo.CanRead && (propertyInfo.PropertyType == typeof(string) || propertyInfo.PropertyType.IsValueType))
            {
                propertyInfo.SetValue(returnedResult.Old, entry.OriginalValues[propertyInfo.Name]);
            }
        }

        foreach (PropertyInfo propertyInfo in typeof(TResult)
                                                .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
                                                .Where(pi => entry.OriginalValues.Properties.Any(property => property.Name == pi.Name)))
        {
            if (propertyInfo.CanRead && (propertyInfo.PropertyType == typeof(string) || propertyInfo.PropertyType.IsValueType))
            {
                propertyInfo.SetValue(returnedResult.New, entry.CurrentValues[propertyInfo.Name]);
            }
        }

        return returnedResult;
    }
}

The breakdown for this class is as follows:

  1. We have two properties of the same type, one representing the Old instance before any modifications were made and the other representing the New state after the modifications have been made.
  2. The factory method CreateFromEntityProperty uses reflection so that we can turn an EntityEntry which into our own entity so it’s easier to work with, since an EntityEntry is not something so easy to interrogate and work with, this will create instances of our entity and copy over the original and current values that are being tracked, but only if they can be written to and are strings or value types (since classes would represent other entities most of the time, excluding owned properties). Additionally, we only look at the properties being tracked.

We will see an example of how this is used in the following section where we see how to implement concrete triggers.

Concrete triggers

We will be creating two triggers to show off how they can differ and also how to register multiple triggers later on when we do the integration into the ServiceProvider.

Attendance trigger

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DbBroadcast.Models; // this is just to point to the `TriggerEntityVersion`, will differ in your system
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;

public class AttendanceTrigger : TriggerBase
{
    private readonly ILogger _logger;

    public AttendanceTrigger(ILogger logger)
    {
        _logger = logger;
    }

    protected override IEnumerable RegisterChangedEntitiesInternal(ChangeTracker changeTracker)
    {
        return changeTracker
                .Entries()
                .Where(entry => entry.State == EntityState.Modified)
                .Select(TriggerEntityVersion.CreateFromEntityProperty);
    }

    protected override Task TriggerAsyncInternal(TriggerEntityVersion trackedTriggerEntity)
    {
        _logger.LogInformation($"Update attendance for user {trackedTriggerEntity.New.Id}");
            return Task.CompletedTask;
    }
}

From the definition of this trigger we can see the following:

  1. This trigger will apply for the entity ApplicationUser.
  2. Since the instance of the trigger is created via ServiceProvider we can inject dependencies via its constructor as we did with the ILogger.
  3. The RegisterChangedEntitiesInternal method implements a query on the tracked entities of type ApplicationUser only if they have been modified. We could check for additional conditions but I would suggest doing that after the .Select call so that you can work with actual instances of your entity.
  4. The TriggerAsyncInternal implementation will just log out the new Id of the user (or any other field we might want).

Ui trigger

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;

using DbBroadcast.Models;

public class UiTrigger : TriggerBase<ApplicationUser>
{
    private readonly ILogger<AttendanceTrigger> _logger;

    public UiTrigger(ILogger<AttendanceTrigger> logger)
    {
        _logger = logger;
    }

    protected override IEnumerable<TriggerEntityVersion<ApplicationUser>> RegisterChangedEntitiesInternal(ChangeTracker changeTracker)
    {
        return changeTracker.Entries<ApplicationUser>().Select(TriggerEntityVersion<ApplicationUser>.CreateFromEntityProperty);
    }

    protected override Task TriggerAsyncInternal(TriggerEntityVersion<ApplicationUser> trackedTriggerEntity)
    {
        _logger.LogInformation($"Update UI for user {trackedTriggerEntity.New.Id}");;
        return Task.CompletedTask;
    }
}

This class is the same as the previous one, this more for example purposes, except it has a different message and also it will track all changes to ApplicationUser entities regardless of their state.

Registering the triggers

Now that we have written up our triggers it’s time to register them. To register multiple implementations of the same interface or base class, all we need to do is make a change in the Startup.ConfigureServices method (or wherever you’re registering your services) as follows:

services.TryAddEnumerable(new []
{
    ServiceDescriptor.Transient<ITrigger, AttendanceTrigger>(), 
    ServiceDescriptor.Transient<ITrigger, UiTrigger>(), 
});

This way you can have triggers of differing lifetimes, as many as you want (though they should be in line with the lifetime of your context, else you will get an error), and easy to maintain. You could even have a configuration file to enable at will certain triggers :D.

Modifing the DbContext

Here I will show two cases which can be useful depending on your requirement. You will also see that the implementation is the same, de difference being a convenience since for simple cases all you need to do is inherit, for complex cases you would need to make these changes manually.

Use a base class

If your context only inherits from DbContext then you could you the following base class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DbBroadcast.Data.Triggers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

public abstract class TriggerDbContext : DbContext
{
    private readonly IServiceProvider _serviceProvider;

    public TriggerDbContext(DbContextOptions<ApplicationDbContext> options, IServiceProvider serviceProvider)
        : base(options)
    {
        _serviceProvider = serviceProvider;
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        IEnumerable<ITrigger> triggers =
            _serviceProvider?.GetServices<ITrigger>()?.ToArray() ?? Enumerable.Empty<ITrigger>();

        foreach (ITrigger userTrigger in triggers)
        {
            userTrigger.RegisterChangedEntities(ChangeTracker);
        }

        int saveResult = await base.SaveChangesAsync(cancellationToken);

        foreach (ITrigger userTrigger in triggers)
        {
            await userTrigger.TriggerAsync();
        }

        return saveResult;
    }
}

Things to point out here are as follows:

  1. We inject the IServiceProvider so that we can reach out to our triggers.
  2. We override the SaveChangesAsync (same would go for all the other save methods of the context, though this one is the most used nowadays) and implement the changes.
    1. We get the triggers from the ServiceProvider (we could even filter them out for a specific trigger type but it’s better to have them as is cause it keeps it simple)
    2. We run through each trigger and save the entities that have changes according to our trigger registration logic.
    3. We run the actual save inside the database to ensure that everything worked properly (is there’s a database error then the trigger would get canceled due to the exception bubbling)
    4. We then run each trigger.
    5. We return the result as if nothing happened :D.

Keep in mind that given this implementation you wouldn’t want to have a trigger that updates the same entity or you might end up in a loop, so either you must have firm rules for your trigger or just don’t change the same entity inside the trigger.

Using your existing context

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DbBroadcast.Data.Triggers;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using DbBroadcast.Models;
using Microsoft.Extensions.DependencyInjection;

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    private readonly IServiceProvider _serviceProvider;

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IServiceProvider serviceProvider)
        : base(options)
    {
        _serviceProvider = serviceProvider;
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        IEnumerable<ITrigger> triggers =
            _serviceProvider?.GetServices<ITrigger>()?.ToArray() ?? Enumerable.Empty<ITrigger>();

        foreach (ITrigger userTrigger in triggers)
        {
            userTrigger.RegisterChangedEntities(ChangeTracker);
        }

        int saveResult = await base.SaveChangesAsync(cancellationToken);

        foreach (ITrigger userTrigger in triggers)
        {
            await userTrigger.TriggerAsync();
        }

        return saveResult;
    }
}

As you can see this is nearly identical to the base class but since this context already inherits from IdentityDbContext then you have you implement your own.

To implement your own you need to both update your constructor to accept a ServiceProvider and override the appropriate save methods.

Conclusion

For this to work we’ve taken advantage of inheritance, the strategy pattern for the triggers, playing with the ServiceProvider and multiple registrations.

I hope you enjoyed this as much as I did tinkering with it, and I’m curious to find out what kinda trigger you might come up with.

Thank you and happy coding,
Vlad V.

2 thoughts on “Application triggers for Asp.Net Core 2.1 entity framework core

Leave a comment