简体   繁体   中英

Entity Framework Core Creation and Update fields

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?

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 )

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.

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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM