8

I have four custom fields on all of my entities. The four fields are CreatedBy, CreatedDate, UpdatedBy and UpdatedDate.

Is there a way to hook into Entity Framework core events so that on an insert it will populate the CreatedDate with the current DateTime and CreatedBy with the current user? When there is an update to the database it would populate UpdatedDate with the current DateTime and UpdatedBy with the current user?

3 Answers 3

19

Basically @Steve's approach is the way to go, but the current implementation of it makes it hard to unit test your project.

With a little bit of refactoring, you can make it unit test friendly and stay true to SOLID principles and encapsulation.

Here's a refactored version of Steve's example

public abstract class AuditableEntity
{
    public DateTime CreatedDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime UpdatedDate { get; set; }
    public string UpdatedBy { get; set; }
}

public class AuditableDbContext : DbContext
{
    protected readonly IUserService userService;
    protected readonly DbContextOptions options;
    protected readonly ITimeService timeService;

    public BaseDbContext(DbContextOptions options, IUserService userService, ITimeService timeService) : base(options)
    {
        userService = userService ?? throw new ArgumentNullException(nameof(userService));
        timeService = timeService ?? throw new ArgumentNullException(nameof(timeService));
    }

    public override int SaveChanges()
    {
        // get entries that are being Added or Updated
        var modifiedEntries = ChangeTracker.Entries()
                .Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified));

        var identityName = userService.CurrentUser.Name;
        var now = timeService.CurrentTime;

        foreach (var entry in modifiedEntries)
        {
            var entity = entry.Entity as AuditableEntity;

            if (entry.State == EntityState.Added)
            {
                entity.CreatedBy = identityName ?? "unknown";
                entity.CreatedDate = now;
            }

            entity.UpdatedBy = identityName ?? "unknown";
            entity.UpdatedDate = now;
        }

        return base.SaveChanges();
    }
}

Now it's easy to mock time and user/principal for unit tests and model/domain/business layer is free of EF Core dependency, better encapsulating your domain logic way better.

Of course one could further refactor this to use a more modular approach by using strategy pattern, but that's out of scope. You can also use ASP.NET Core Boilerplate which also offers an implementation of an auditable (and soft delete) EF Core DbContext (here and here)

Sign up to request clarification or add additional context in comments.

5 Comments

I like the idea of this. One problem could be if you have an entity that's not auditable, or a different kind of on save settings, than this approach won't work, unless you maintain several dbcontexts of different types.
@Steve: ChangeTracker.Entries() .TypeOf<AuditableEntity>() already filters the auditable entities. But if one would want to modularize it further, one can use strategy pattern and split that into multiple IEntitySaveHandler<T>s or IEntitySaveHandlers and then have to methods on it: CanHandle and Handle methods. Then each handler could check if it can be applied to the entity and if so, perform the changes. This approach would also honor the open/closed principle (open for extension, closed for modification) and you could add new handlers w/o modifying the base DbContext
But it's a bit out of scope for a single question
@Tseng would this type of change need to be applied to all Save methods (SaveChanges, SaveChangesAsync etc.) in case users use different methods? Or is there a single method called by all these that this solution can be applied to which then is consumed by varied methods?
@Aeseir: You'll need to apply it to both methods, since there is not a common one as far as I know
4

I have exactly the same layout as you with what I call "Audit" fields.

The way I solved this was to create a base abstract class called AuditableEntity to hold the properties themselves and expose a method called PrepareSave. Inside PrepareSave I set the values of the fields as required:

public abstract class AuditableEntity
{
    public DateTime CreatedDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime UpdatedDate { get; set; }
    public string UpdatedBy { get; set; }

    public virtual void PrepareSave(EntityState state)
    {
        var identityName = Thread.CurrentPrincipal.Identity.Name;
        var now = DateTime.UtcNow;

        if (state == EntityState.Added)
        {
            CreatedBy = identityName ?? "unknown";
            CreatedDate = now;
        }

        UpdatedBy = identityName ?? "unknown";
        UpdatedDate = now;
    }
}

I made PrepareSave virtual so I can override it in my entities if I want. You may need to change how you get the Identity depending on your implementation.

To call this, I overwrote SaveChanges on my DbContext and called PrepareSave on each entity that was being added or updated (which I got from the change tracker):

public override int SaveChanges()
{
    // get entries that are being Added or Updated
    var modifiedEntries = ChangeTracker.Entries()
            .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

    foreach (var entry in modifiedEntries)
    {
        // try and convert to an Auditable Entity
        var entity = entry.Entity as AuditableEntity;
        // call PrepareSave on the entity, telling it the state it is in
        entity?.PrepareSave(entry.State);
    }

    var result = base.SaveChanges();
    return result;
}

Now, whenever I call SaveChanges on my DbContext (either directly or through a repository), any entity that inherits AuditableEntity will have it's audit fields set as necessary.

5 Comments

You can also package this functionality into an Interface IAuditableEntity so that you don't have to force your entity classes to all inherit from a base class
@marc_s I do have an interface (actually I have a couple more levels) but I omitted it for brevity, since AuditableEntity contains the relevant logic to set the fields.
It's not the optimal solution. PrepareSave method violates against at least 2 best practices: 1) SRP from SOLID: Single Responsibility Principle, because the AuditableEntity has more than one responsibility: tracking of the fields to update and holding it's own state as well as violation of separation of concerns. 2) it also violates the encapsulation: With declaring EntityStateas a parameter of the public method on all of your entities, you have now tightly coupled **all of your ** layers such as Domain/Business layer with a persistance technology
You should move all of your code inside SaveChanges method and inject IHttpContextAccessor (or even better: A service which access the HttpContext and returns the logged in principal) into your DbContext base class. Then you have clean decoupling and no persistence related code in your models. Without it, it makes your models much harder to uni test also since datetime/thread access can't be mocked
@Tseng I'm not pretending it's the best solution, as with most things Entity Framework there are several ways to do things and this works for us. Ultimately, this is just part of the code base I'm using that contains a significant amount of other base classes and customisations to how Entity Framework works, since our requirements are complex enough to not work well with out of the box solutions. Not all of the entities implement AuditableEntity and there are other base classes they might implement, hence this approach. Feel free to add an answer with your suggestion.
0

Unfortunately my reputation is to low for adding comments. For this reason I add my Answer. So: one addition to Tseng Answer. When entity in modified state You must set IsModidied = false tor CreatedBy and CreatedDate properties. Otherwise that fields will be rewritten by optional values for string and DateTime types.

if (entry.State == EntityState.added)
{
    CreatedBy = identityName ?? "unknown";
    CreatedDate = now;
}
else
{
    Entry<AuditableEntity>(entry).Property(p => p.CreatedBy).IsModified = false;
    Entry<AuditableEntity>(entry).Property(p => p.CreatedDate).IsModified = false;
    // or
    // entity.Property("CreatedBy").IsModified = false;
    // entity.Property("CreatedDate").IsModified = false;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.