C# Save History Tables Interface with Entity Framework with Dependency Injection

Viewed 1053

We are using Net Core 2 with Entity Framework. Our Sql database consists of many tables, Address, Account, Customer, Sales, etc.

Some tables have corresponding history tables: AddressHistory, CustomerHistory,

Anytime someone changes something in original tables, the corresponding History table should be updated. (cannot use sql temporal tables, since we have custom history logic)

We are trying to apply interfaces, I'm little stuck on the code, can someone provide a quick code example of how to implement this with interfaces? Especially in SaveHistory portion, how can dependency injection be applied? Feel free to rewrite code as needed

public partial class TestDbContext
{

    public void AddEntityHistory<IEntityHistory>(IEntityHistory entity) where IEntityHistory: Address
    {  
       // do something 1 etc
    }

    public void AddEntityHistory<IEntityHistory>(IEntityHistory entity) where IEntityHistory: Customer
    {  
       // do something 2 etc
    }

    public override int SaveChanges()
    {
        SaveEntityHistory();
        return base.SaveChanges();
    }

    protected void SaveEntityHistory()
    {
         var modifiedEntities = ChangeTracker.Entries<IEntityHistory>().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
         foreach(var entity in modifiedEntities)
         { 
              AddEntityHistory(entity);  // what is the code base here? giving error below, it should call appropriate AddEntityHistory Method for corresponding Entity
         }
    }
}

Error Above:

Error CS0311 The type 'Interfaces.IEntityHistory' cannot be used as type parameter 'IEntityHistory' in the generic type or method 'PropertyContext.AddEntityHistory(IEntityHistory)'. There is no implicit reference conversion from 'Interfaces.IEntityHistory' to 'Data.Entities.Address'.

Resources :

Trying to utilize similar codebase: for CreateDate, UserId, etc

https://dotnetcore.gaprogman.com/2017/01/26/entity-framework-core-shadow-properties/

2 Answers

Generic form for methods won't work here at all. Reflection is more suitable for your requirement:

public partial class TestDbContext
{

    public void AddEntityHistory(Address entity)
    {
        // do something 1 etc
    }

    public void AddEntityHistory(Customer entity)
    {
        // do something 2 etc
    }

    public override int SaveChanges()
    {
        SaveEntityHistory();
        return base.SaveChanges();
    }

    protected void SaveEntityHistory()
    {
        var modifiedEntities = ChangeTracker.Entries<IEntityHistory>()
            .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
        foreach (var entity in modifiedEntities)
        {
            var methodInfo = this.GetType().GetMethod(nameof(AddEntityHistory), new[] { entity.Entity.GetType() });
            methodInfo.Invoke(this, new[] { entity.Entity });
        }
    }
}

You can use a Generic Repository and then for each Entity's repository, you can then save its respective history table. Below is the example code.

IGenericRepository

public interface IGenericRepository
    {
        Task GetByIdAsync(object id, CancellationToken cancellationToken = default);
        Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default);
        void Delete(object id);
        void Delete(TEntity entityToDelete);
        void Update(TEntity entityToUpdate);
        void UpdateStateAlone(TEntity entityToUpdate);
    }

GenericRepository

public class GenericRepository : IGenericRepository
   where TEntity : class, new()
    {
        private readonly YourDbContext context;
        internal DbSet dbSet;

        public GenericRepository(YourDbContext context)
        {
            this.context = context;
            dbSet = context.Set();
        }

        public virtual Task GetByIdAsync(object id, CancellationToken cancellationToken = default)
        {
            return dbSet.FindAsync(id);
        }

        public virtual Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
        {
            return dbSet.AddAsync(entity, cancellationToken);
        }

        public virtual void Delete(object id)
        {
            TEntity entityToDelete = dbSet.Find(id);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            dbSet.Attach(entityToUpdate);
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }

        public virtual void UpdateStateAlone(TEntity entityToUpdate)
        {            
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }

    }

Now, to use the above Generic Repository for your Entities, use the below sample code.

using System.Threading.Tasks;

public interface IAddressRepository: IGenericRepository
    {
        Task CommitAsync();
public virtual Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
    }
using System.Threading.Tasks;

 public class AddressRepository: GenericRepository, IAddressRepository
    {
        private readonly YourDbContext _context;
        public AddressRepository(YourDbContext context) : base(context)
        {
            _context = context;
        }

     public override Task InsertAsync(Address entity, CancellationToken cancellationToken = default)
        {
base.InsertAsync(entity,cancellationToken );
//call your history insert here and then the below save. This will save both the record in the main Address table and then your Address's history table.
            return _context.SaveChangesAsync();
        }

public Task CommitAsync()
        {
            return _context.SaveChangesAsync();
        }
    }

For detailed implementation, refer this Implement create, read, update, and delete functionalities using Entity Framework Core

Related