8
votes

I'm new to Entity Framework, in the past I've used Enterprise Library or ADO.NET directly to map models to database tables. One pattern that I've used is to put my common audit fields that appear in every table in a Base Class and then inherit that Base Class for every object.

I take two steps to protect two of the fields (Created, CreatedBy):

  1. Have a parameterless constructor private on the Base Enitity and create a second one that requires Created and CreatedBy are passed on creation.
  2. Make the setters Private so that the values cannot be changed after the object is created.

Base Class:

using System;

namespace App.Model
{
    [Serializable()]
    public abstract class BaseEntity
    {
        public bool IsActive { get; private set; }
        public DateTimeOffset Created { get; private set; }
        public string CreatedBy { get; private set; }
        public DateTimeOffset LastUpdated { get; protected set; }
        public string LastUpdatedBy { get; protected set; }

        private BaseEntity() { }

        protected BaseEntity(DateTimeOffset created, string createdBy)
        {
            IsActive = true;
            Created = created;
            CreatedBy = createdBy;
            LastUpdated = created;
            LastUpdatedBy = createdBy;
        }
    }
}

Inherited Class:

using System;

namespace App.Model
{
    [Serializable()]
    public class Person : BaseEntity
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }

        public Person(DateTimeOffset created, string createdBy) : 
            base(created, createdBy) {  }
    }
}

I've run into issues with both. EF requires a parameterless constructor to create objects. EF will not create the database columns that have a private setter.

My question is if there is a better approach to accomplish my goals with EF:

  1. Require that the values for Created and CreatedBy are populated at instantiation.
  2. The values of Created and CreatedBy cannot be changed.
2
There's another option: override SaveChanges in your context and do all audit-related actions there (setting creation date on new entities, setting modification date on updated entities etc). It will all be done just before save. - Patryk Ćwiek
I would implement an interface IAuditiable instead, that way you can still have entities that shouldnt be audited, and the ones that should you can implement logic specific to that interface in your SaveChanges() call like suggested in one of the answers below, if thats what you want. - Jim Wolff
@FRoZeN, you cannot have non-public setters from on an Interface so that wouldn't meet my second goal. - Josh

2 Answers

5
votes

You could instantiate a context with a constructor that accepts a string createdBy. Then in an override of SaveChanges():

public override int SaveChanges()
{
    foreach( var entity in ChangeTracker.Entries()
                                        .Where(e => e.State == EntityState.Added)
                                        .Select (e => e.Entity)
                                        .OfType<BaseEntity>())
    {
        entity.SetAuditValues(DateTimeOffset.Now, this.CreatedBy);
    }
    return base.SaveChanges();
}

With SetAuditValues() as

internal void SetAuditValues(DateTimeOffset created, string createdBy)
{
    if (this.Created == DateTimeOffset.MinValue) this.Created = created;
    if (string.IsNullOrEmpty(this.CreatedBy)) this.CreatedBy = createdBy;
}

After the entities have been materialized from the database the values won't be overwritten when someone calls SetAuditValues.

0
votes

You shouldn't be trying to control access rights directly on your entities/data layer instead your should do this in your application layer. This way you have a finer level of control over what users can do what.

Also rather than have the audit fields repeated on every table you might want to store your Audit records in another table. This is easy to do with code first:

public class AuitRecord
{
    public bool IsActive { get; set; }
    public DateTimeOffset Created { get; set; }
    public string CreatedBy { get; set; }
    public DateTimeOffset LastUpdated { get; set; }
    public string LastUpdatedBy { get; set; }
}

You would then link the base class with the audit record to it:

public abstract class BaseEntity
{
    public AuditRecord Audit { get; set; }
}

And finally your actually entities

public class Person : BaseEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

You can no access the audit data by going:

Person.Audit.IsActive