Search code examples
c#dependency-injectionautofacwrapper

How to set Created By to enitites in DbContext


Further to the Stack Overflow question How to set created date and Modified Date to enitites in DB first approach asked by user LP13 and answered by user Ogglas.

I am writing a test project to learn new development approaches and have hit a wall. I am trying to implement the answer provided by Ogglas, however I am unsure how to register the "Wrapper" in AutoFac?

Ogglas's and My Code Example

public interface IEntity
{
    DateTime CreatedDate { get; set; }

    string CreatedBy { get; set; }

    DateTime UpdatedDate { get; set; }

    string UpdatedBy { get; set; }
}

public interface IAuditableEntity
{
    DateTime CreatedDate { get; set; }
    string CreatedBy { get; set; }
    DateTime UpdatedDate { get; set; }
    string UpdatedBy { get; set; }
}

public interface ICurrentUser
{
    string GetUsername();
}

public interface ICurrentUser
{
    string Name();
    string GetUserId();
    bool IsUserAuthenticated();
    bool IsUserAdmin();
    bool IsUserManager();
}

public class ApplicationDbContextUserWrapper
{
    public ApplicationDbContext Context;

    public ApplicationDbContextUserWrapper(ApplicationDbContext context, ICurrentUser currentUser)
    {
        context.CurrentUser = currentUser;
        this.Context = context;
    }
}

public class MyDbContextWrapper
{
    public IMyDbContext Context;

    public MyDbContextWrapper(IMyDbContext context, ICurrentUser currentUser)
    {
        context.CurrentUser = currentUser;
        Context = context;
    }
}

public class ApplicationDbContext : DbContext
{

    public ICurrentUser CurrentUser;

    public override int SaveChanges()
    {
        var now = DateTime.Now;

        foreach (var changedEntity in ChangeTracker.Entries())
        {
            if (changedEntity.Entity is IEntity entity)
            {
                switch (changedEntity.State)
                {
                    case EntityState.Added:
                        entity.CreatedDate = now;
                        entity.UpdatedDate = now;
                        entity.CreatedBy = CurrentUser.GetUsername();
                        entity.UpdatedBy = CurrentUser.GetUsername();
                        break;
                    case EntityState.Modified:
                        Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                        Entry(entity).Property(x => x.CreatedDate).IsModified = false;
                        entity.UpdatedDate = now;
                        entity.UpdatedBy = CurrentUser.GetUsername();
                        break;
                }
            }
        }

        return base.SaveChanges();
    }
}

public class MyDbContext : DbContext, IMyDbContext
{
    public ICurrentUser CurrentUser { get; set; }

    public DbSet<Staff> Staff { get; set; }
    public DbSet<AddressStaff> StaffAddresses { get; set; }

    public MyDbContext() : base("Name=MyWebPortalConnection")
    {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyDbContext, MyWebPortalContextMigrationConfiguration>());
    }

    public override int SaveChanges()
    {
        var modifiedEntries = ChangeTracker.Entries().Where(x => x.Entity is IAuditableEntity
                && (x.State == EntityState.Added
                || x.State == EntityState.Modified));

        foreach (var entry in modifiedEntries)
        {
            if (entry.Entity is IAuditableEntity entity)
            {
                var dateTimeZone = DateTimeZoneProviders.Tzdb["Europe/London"];
                var zonedClock = SystemClock.Instance.InZone(dateTimeZone);
                var localDateTime = zonedClock.GetCurrentLocalDateTime();
                var dateTime = new DateTime(localDateTime.Year,
                                            localDateTime.Month,
                                            localDateTime.Day,
                                            localDateTime.Hour,
                                            localDateTime.Minute,
                                            localDateTime.Second);

                if (entry.State == EntityState.Added)
                {
                    entity.CreatedBy = CurrentUser.Name();
                    entity.CreatedDate = dateTime;
                }
                else if (entry.State == EntityState.Modified)
                {
                    entity.UpdatedBy = CurrentUser.Name();
                    entity.UpdatedDate = dateTime;
                }
                else
                {
                    Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                    Entry(entity).Property(x => x.CreatedDate).IsModified = false;
                }
            }
        }

        return base.SaveChanges();
    }

My AutoFac EF Module Updated

public class EFModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        //builder.RegisterType<MyDbContextWrapper>().As<IMtDbContext>();
        //builder.RegisterDecorator<MyDbContextWrapper, IMyDbContext>();
        //builder.RegisterDecorator<MyDbContextWrapper, IMyDbContext>();
        builder.RegisterType<MyDbContextWrapper>().AsSelf().InstancePerLifetimeScope();
        builder.RegisterType(typeof(MyDbContext)).As(typeof(IMyDbContext)).As(typeof(DbContext)).InstancePerLifetimeScope();
        builder.RegisterType(typeof(UnitOfWork)).As(typeof(IUnitOfWork)).InstancePerRequest();
        builder.Register(_ => new HttpClient()).As<HttpClient>().InstancePerLifetimeScope();
    }
}

I have used the following tutorial as a guide to setting up my project Tutorial Guide Project I would very much appreciate any assistance given. Thank you.

Generic Repository Updated

public abstract class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity
{
    protected MyDbContextWrapper DbContextWrapper;
    protected DbContext GenericDbContext;
    protected readonly IDbSet<T> GenericDbset;

    protected GenericRepository(MyDbContextWrapper dbContextWrapper)
    {
        DbContextWrapper = dbContextWrapper;
        GenericDbContext = (DbContext)DbContextWrapper.Context;
        GenericDbset = GenericDbContext.Set<T>();
    }

IMyDbContext Updated

public interface IMyDbContext
{
    ICurrentUser CurrentUser { get; set; }
    DbSet<Staff> Staff { get; set; }
    DbSet<AddressStaff> StaffAddresses { get; set; }

    int SaveChanges();
}

My CurrentUser AutoFac Module

 public class CurrentUserModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterAssemblyTypes(Assembly.Load("MyWebPortal.Model"))
               .Where(t => t.Name.EndsWith("User"))
               .AsImplementedInterfaces()
               .InstancePerLifetimeScope();
    }
}

Solution

  • Here's a minimal working solution.

    Interfaces:

    public interface IMyDbContext
    {
        DbSet<Staff> Staff { get; }
        DbChangeTracker ChangeTracker { get; }
        int SaveChanges();
    }
    
    public interface IAuditableEntity
    {
        string CreatedBy { get; set; }
    }
    
    public interface ICurrentUser
    {
        string Name();
    }
    

    Entity:

    public class Staff : IAuditableEntity
    {
        [Key]
        public int Id { get; set; }
        public string CreatedBy { get; set; }
    }
    

    Mocks:

    public class MockCurrentUser : ICurrentUser
    {
        public string Name() => "Mock";
    }
    
    public class MockDbContext : DbContext, IMyDbContext
    {
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            Database.SetInitializer<MockDbContext>(null);
            base.OnModelCreating(modelBuilder);
        }
    
        public DbSet<Staff> Staff { get; set; }
    
        public override int SaveChanges() => 1;
    }
    

    Decorator:

    public class ApplicationDbContextAuditDecorator : IMyDbContext
    {
        private readonly IMyDbContext context;
        private readonly ICurrentUser currentUser;
    
        public ApplicationDbContextAuditDecorator(IMyDbContext context, ICurrentUser currentUser)
        {
            this.context = context;
            this.currentUser = currentUser;
        }
        public DbSet<Staff> Staff { get => this.context.Staff; }
    
        public DbChangeTracker ChangeTracker => this.context.ChangeTracker;
    
        public int SaveChanges()
        {
            foreach (var changedEntity in ChangeTracker.Entries())
            {
                if (changedEntity.Entity is IAuditableEntity entity)
                {
                    switch (changedEntity.State)
                    {
                        case EntityState.Added:
                            entity.CreatedBy = this.currentUser.Name();
                            break;
                    }
                }
            }
    
            return this.context.SaveChanges();
        }
    }
    

    And test:

    [TestMethod]
    public void TestMethod1()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<MockDbContext>().As<IMyDbContext>().InstancePerLifetimeScope();
        builder.RegisterDecorator<ApplicationDbContextAuditDecorator, IMyDbContext>();
        builder.RegisterType<MockCurrentUser>().As<ICurrentUser>();
    
        var container = builder.Build();
        var context = container.Resolve<IMyDbContext>();
        context.Staff.Add(new Staff());
        context.SaveChanges();
    
        Assert.AreEqual("Mock", context.Staff.Local.Single().CreatedBy);
    }