Audit.EntityFramework with Simple Injector On .Net Core

1k Views Asked by At

I'm currently using .net core 2.0 along with Audit.EntityFramework to audit changes on my entities.

I've gone the route of creating my own CustomProvider as explained here DataProviders brief excerpt below.

EmployerCustomDataProvider

public class EmployerCustomDataProvider : AuditDataProvider, IEmployerCustomDataProvider {
    private readonly IUnitOfWork _unitOfWork;

    public EmployerCustomDataProvider (IUnitOfWork unitOfWork, IMapper mapper) {
        _unitOfWork = unitOfWork;
    }

    // Async Implementation
    public override async Task<object> InsertEventAsync (AuditEvent auditEvent) {
            // Unique Id For Audit Event
            var Id = Guid.NewGuid ();
            EntityFrameworkEvent auditEntityFrameworkEvent = auditEvent.GetEntityFrameworkEvent ();

            // Need to audit each entry
            List<EventEntry> eventEntries = auditEntityFrameworkEvent.Entries;

            foreach (EventEntry entry in eventEntries) {
                EmployerAuditHistory employerAuditHistory = Mapper.Map<EmployerAuditHistory> (entry);
                this._unitOfWork.EmployerAuditHistoryRepository.Insert (employerAuditHistory);

                await this._unitOfWork.CommitAsync ();

                foreach (EventEntryChange change in entry.Changes) {
                    EmployerAuditHistoryDetail employerAuditHistoryDetail = new EmployerAuditHistoryDetail ();
                    employerAuditHistoryDetail.EmployerAuditHistoryId = employerAuditHistory.EmployerAuditHistoryId;
                    employerAuditHistoryDetail.ColumnName = change.ColumnName;
                    employerAuditHistoryDetail.OriginalValue = change.OriginalValue.ToString ();
                    employerAuditHistoryDetail.NewValue = change.NewValue.ToString ();

                    this._unitOfWork.EmployerAuditHistoryDetailRepository.Insert (employerAuditHistoryDetail);
                }
            }

            return await Task.Run (() => Id);
        }

        ...

According to the documentation this custom provider is called under the following conditions.

This library intercepts calls to SaveChanges() / SaveChangesAsync() on your DbContext, to generate Audit.NET events with the affected entities information.

Each call to SaveChanges generates an audit event that includes information of all the entities affected by the save operation.

In my business layer I have a method like this that Creates or Updates an employer branch.

EmployerBranchService : Business Layer

public class EmployerBranchService : BaseService, IEmployerBranchService {
private readonly IUnitOfWork _unitOfWork;

public EmployerBranchService (IMapper mapper, IUnitOfWork unitOfWork) : base (mapper) {
    this._unitOfWork = unitOfWork;
}
private async Task<EmployerBranchModel> CreateUpdateEmployerBranch (EmployerBranchModel employerBranchModel) {
    EmployerBranch employerBranch = Mapper.Map (employerBranchModel, await this._unitOfWork.EmployerBranchRepository.GetEmployerBranchById (employerBranchModel.EmployerBranchId.Value));

    if ((employerBranch?.EmployerBranchId ?? 0) == 0)
        this._unitOfWork.EmployerBranchRepository.Insert (employerBranch);
    else
        this._unitOfWork.EmployerBranchRepository.Update (employerBranch);

    await this._unitOfWork.CommitAsync ();

    return Mapper.Map<EmployerBranchModel> (employerBranch);
    }
}

When my unit of works commit is called the branch saves as expected and i land up in the EmployerCustomDataProvider classes InsertEventAsync method. However when i call the unit of work here i get the following error.

Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'EmployerContext'.

It appears to me the problem is that when the business layers Unit Of Work submits it's changes, the context gets disposed. I'm not sure how to get around this as i have tried registering my dependencies differently etc.

I've tried registering the audit setup in my program method.

Program

  public class Program
{

    public static void Main(string[] args)
    {
        IWebHost host = BuildWebHost(args);
        SeedDatabase(host);
        SetupAudit(host);
        host.Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();

    //Seed database methods in .net core need to be done in the Program method:
    //https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro
    public static void SeedDatabase(IWebHost host)
    {
        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            try
            {
                EmployerContext context = services.GetRequiredService<EmployerContext>();
                DbInitializer.Seed(context);
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred while seeding the database.");
            }
        }
    }

    public static void SetupAudit(IWebHost host)
    {
        var _container = DependencyInjectionConfig.Container;

        using (AsyncScopedLifestyle.BeginScope(_container))
        {
            AuditConfiguration.SetupAuditConfiguration(_container);
        }
    }
}

DependencyInjectionConfig

public class DependencyInjectionConfig
{
    private static Container _container;

    public static Container Container
    {
        get
        {
            if (_container == null)
            {
                _container = new Container();
                _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
            }

            return _container;
        }
    }

    public static void InitializeContainer(IApplicationBuilder app)
    {
        Container.RegisterMvcControllers(app);
        Container.RegisterMvcViewComponents(app);
        Container.CrossWire<EmployerContext>(app);

        Container.Register<ITypeService, TypeService>(Lifestyle.Scoped);
        Container.Register<IEmployerService, EmployerService>(Lifestyle.Scoped);
        Container.Register<IEmployerPersonContactService, EmployerPersonContactService>(Lifestyle.Scoped);
        Container.Register<IEmployerBranchService, EmployerBranchService>(Lifestyle.Scoped);
        Container.Register<IEmployerSearchService, EmployerSearchService>(Lifestyle.Scoped);
        Container.Register<INoteService, NoteService>(Lifestyle.Scoped);

        Container.Register<IBaseModelUsernameResolver, BaseModelUsernameResolver>(Lifestyle.Scoped);

        Container.Register<IUnitOfWork, UnitOfWork>(Lifestyle.Scoped);
        Container.Register<IEmployerNoteTypeRepository, EmployerNoteTypeRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerDocumentTypeRepository, EmployerDocumentTypeRepository>(Lifestyle.Scoped);
        Container.Register<IPrivatePayrollRepository, PrivatePayrollRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerRepository, EmployerRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerContactDetailsRepository, EmployerContactDetailsRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerAddressRepository, EmployerAddressRepository>(Lifestyle.Scoped);
        Container.Register<IUserRepository, UserRepository>(Lifestyle.Scoped);
        Container.Register<ISearchRepository, SearchRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerBranchRepository, EmployerBranchRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerBranchContactDetailsRepository, EmployerBranchContactDetailsRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerBranchAddressRepository, EmployerBranchAddressRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerPersonContactRepository, EmployerPersonContactRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerBranchSummaryRepository, EmployerBranchSummaryRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerAuditHistoryRepository, EmployerAuditHistoryRepository>(Lifestyle.Scoped);
        Container.Register<INoteRepository, NoteRepository>(Lifestyle.Scoped);
        Container.Register<IEmployerAuditHistoryDetailRepository, EmployerAuditHistoryDetailRepository>(Lifestyle.Scoped); 

        Container.Register<IEmployerCustomDataProvider, EmployerCustomDataProvider>(Lifestyle.Scoped);

        Container.AutoCrossWireAspNetComponents(app);
        Container.Verify();
    }

    public static void Register<TService, TImplementation>() where TService : class where TImplementation : class, TService
    {
        Container.Register<TService, TImplementation>();
    }
}

AuditConfiguration

 public static class AuditConfiguration
{
    public static void SetupAuditConfiguration(SimpleInjector.Container container)
    {
        Configuration.Setup()
            .UseCustomProvider((AuditDataProvider)container.GetInstance<IEmployerCustomDataProvider>())
            .WithCreationPolicy(EventCreationPolicy.InsertOnEnd);

        Audit.EntityFramework.Configuration.Setup()
                    .ForContext<EmployerContext>(config => config
                        .IncludeEntityObjects()
                        .AuditEventType("{context}:{database}"))
                    .UseOptOut();
    }
}

UnitOfWork

 public class UnitOfWork : IUnitOfWork
{
    private readonly EmployerContext _context;
    private readonly IEmployerRepository _employerRepository;
    private readonly IEmployerContactDetailsRepository _employerContactDetailsRepository;
    private readonly IEmployerAddressRepository _employerAddressRepository;
    private readonly IEmployerPersonContactRepository _employerPersonContactRepository;
    private readonly IEmployerBranchRepository _employerBranchRepository;
    private readonly IEmployerBranchContactDetailsRepository _employerBranchContactDetailsRepository;
    private readonly IEmployerBranchAddressRepository _employerBranchAddressRepository;

    private readonly IEmployerDocumentTypeRepository _employerDocumentTypeRepository;
    private readonly IEmployerNoteTypeRepository _employerNoteTypeRepository;
    private readonly IPrivatePayrollRepository _privatePayrollRepository;
    private readonly IEmployerBranchSummaryRepository _employerBranchSummaryRepository;
    private readonly INoteRepository _employerBranchNoteRepository;
    private readonly ISearchRepository _searchRepository;
    private readonly IEmployerAuditHistoryRepository _employerAuditHistoryRepository;
    private readonly IEmployerAuditHistoryDetailRepository _employerAuditHistoryDetailRepository;

    public IEmployerRepository EmployerRepository => this._employerRepository;
    public IEmployerContactDetailsRepository EmployerContactDetailsRepository => this._employerContactDetailsRepository;
    public IEmployerAddressRepository EmployerAddressRepository => this._employerAddressRepository;
    public IEmployerPersonContactRepository EmployerPersonContactRepository => this._employerPersonContactRepository;
    public IEmployerBranchRepository EmployerBranchRepository => this._employerBranchRepository;
    public IEmployerBranchContactDetailsRepository EmployerBranchContactDetailsRepository => this._employerBranchContactDetailsRepository;
    public IEmployerBranchAddressRepository EmployerBranchAddressRepository => this._employerBranchAddressRepository;
    public IEmployerDocumentTypeRepository EmployerDocumentTypeRepository { get => this._employerDocumentTypeRepository; }
    public IEmployerNoteTypeRepository EmployerNoteTypeRepository { get => this._employerNoteTypeRepository; }
    public IPrivatePayrollRepository PrivatePayrollRepository { get => this._privatePayrollRepository; }
    public IEmployerBranchSummaryRepository EmployerBranchSummaryRepository { get => this._employerBranchSummaryRepository; }
    public INoteRepository EmployerBranchNoteRepository { get => this._employerBranchNoteRepository; }
    public ISearchRepository SearchRepository { get => this._searchRepository; }
    public IEmployerAuditHistoryRepository EmployerAuditHistoryRepository { get => this._employerAuditHistoryRepository; }
    public IEmployerAuditHistoryDetailRepository EmployerAuditHistoryDetailRepository { get => this._employerAuditHistoryDetailRepository; }

    public UnitOfWork(EmployerContext context,
        IEmployerRepository employerRepository,
        IEmployerContactDetailsRepository employerContactDetailsRepository,
        IEmployerAddressRepository employerAddressRepository,
        IEmployerPersonContactRepository employerBranchPersonRepository,
        IEmployerBranchRepository employerBranchRepository,
        IEmployerBranchContactDetailsRepository employerBranchContactDetailsRepository,
        IEmployerBranchAddressRepository employerBranchAddressRepository,
        IEmployerDocumentTypeRepository employerDocumentTypeRepository,
        IEmployerNoteTypeRepository employerNoteTypeRepository,
        IPrivatePayrollRepository privatePayrollRepository,
        IEmployerBranchSummaryRepository employerBranchSummaryRepository,
        INoteRepository EmployerBranchNoteRepository,
        ISearchRepository SearchRepository,
        IEmployerAuditHistoryRepository EmployerAuditHistoryRepository,
        IEmployerAuditHistoryDetailRepository EmployerAuditHistoryDetailRepository)
    {
        this._employerRepository = employerRepository;
        this._employerContactDetailsRepository = employerContactDetailsRepository;
        this._employerAddressRepository = employerAddressRepository;
        this._employerPersonContactRepository = employerBranchPersonRepository;
        this._employerBranchRepository = employerBranchRepository;
        this._employerBranchContactDetailsRepository = employerBranchContactDetailsRepository;
        this._employerBranchAddressRepository = employerBranchAddressRepository;

        this._employerDocumentTypeRepository = employerDocumentTypeRepository;
        this._employerNoteTypeRepository = employerNoteTypeRepository;
        this._privatePayrollRepository = privatePayrollRepository;
        this._employerBranchSummaryRepository = employerBranchSummaryRepository;
        this._employerBranchNoteRepository = EmployerBranchNoteRepository;
        this._searchRepository = SearchRepository;
        this._employerAuditHistoryRepository = EmployerAuditHistoryRepository;
        this._employerAuditHistoryDetailRepository = EmployerAuditHistoryDetailRepository;

        this._context = context;
    }

    public void Commit()
    {
        try
        {
            this._context.SaveChanges();
        }
        catch (Exception)
        {
            throw;
        }
    }

    public async Task CommitAsync()
    {
        try
        {
            await this._context.SaveChangesAsync();
        }
        catch (Exception e)
        {
            throw;
        }
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                this._context.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

EmployerBranchRepository

 public class EmployerBranchRepository : RepositoryBase<EmployerBranch>, IEmployerBranchRepository
{
    public EmployerBranchRepository(EmployerContext context) : base(context)
    {

    }

    public async Task<EmployerBranch> GetEmployerBranchById(int employerBranchId)
    {
        return await base.FindAsync(e => e.IsActive && e.EmployerBranchId == employerBranchId);
    }

    public async Task<IList<EmployerBranch>> GetEmployerBranchesByEmployerId(int employerId)
    {
        return await base.FindAll(e => e.IsActive && e.EmployerId == employerId).ToListAsync();
    }
}

RepositoryBase

  public abstract class RepositoryBase<TEntity> : RepositoryReadOnlyBase<TEntity>, IRepository<TEntity> where TEntity : class
{
    /// <summary>
    /// Initializes a new instance of the <see cref="RepositoryBase{TEntity}"/> class.
    /// </summary>
    /// <param name="unitOfWork">The unit of work.</param>
    /// <exception cref="NullReferenceException">Unit Of Work cannot be null</exception>
    public RepositoryBase(EmployerContext context) : base(context)
    {

    }

    /// <summary>
    /// Inserts the specified entity.
    /// </summary>
    /// <param name="entity">The entity.</param>
    public void Insert(TEntity entity)
    {
        this._dbSet.Add(entity);
    }

    /// <summary>
    /// Updates the specified entity.
    /// </summary>
    /// <param name="entity">The entity.</param>
    public void Update(TEntity entity)
    {
        this._dbSet.Attach(entity);
        this._dbContext.Entry(entity).State = EntityState.Modified;

        PropertyEntry InsertedUserId = this._dbContext.Entry(entity).Property("InsertedUserId");
        if (InsertedUserId != null)
            InsertedUserId.IsModified = false;
        PropertyEntry InsertedDate = this._dbContext.Entry(entity).Property("InsertedDate");
        if (InsertedDate != null)
            InsertedDate.IsModified = false;
    }
}

RepositoryReadOnlyBase

public class RepositoryReadOnlyBase<TEntity> : IRepositoryReadOnly<TEntity> where TEntity : class
{
    public readonly DbSet<TEntity> _dbSet;
    public readonly EmployerContext _dbContext;

    /// <summary>
    /// Initializes a new instance of the <see cref="RepositoryBase{TEntity}"/> class.
    /// </summary>
    /// <param name="unitOfWork">The unit of work.</param>
    /// <exception cref="NullReferenceException">Unit Of Work cannot be null</exception>
    public RepositoryReadOnlyBase(EmployerContext context)
    {
        this._dbContext = context;
        this._dbSet = this._dbContext.Set<TEntity>();
    }

    /// <summary>
    /// Finds all asynchronous.
    /// </summary>
    /// <param name="predicate">The predicate.</param>
    /// <param name="orderBy">The order by.</param>
    /// <returns></returns>
    public IQueryable<TEntity> FindAll(Expression<Func<TEntity, bool>> predicate = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, params Expression<Func<TEntity, object>>[] includes)
    {
        IQueryable<TEntity> query = this._dbContext.Set<TEntity>();

        if (includes != null)
        {
            query = query = includes.Aggregate(query,
              (current, include) => current.Include(include));
        }

        if (predicate != null)
            query = query.Where(predicate);

        if (orderBy != null)
            query = orderBy(query);

        return query;
    }

    /// <summary>
    /// Finds the asynchronous.
    /// </summary>
    /// <param name="predicate">The predicate.</param>
    /// <param name="orderBy">The order by.</param>
    /// <returns></returns>
    public async Task<TEntity> FindAsync(Expression<Func<TEntity, bool>> predicate = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, params Expression<Func<TEntity, object>>[] includes)
    {
        return await this.FindAll(predicate, orderBy, includes).FirstOrDefaultAsync();
    }

    public Task<TEntity> FindByIdAsync(int id)
    {
        return this._dbSet.FindAsync(id);
    }
}

I'm not sure how much detail to include but can include extracts of the unit of work etc. Any direction/advice is appreciated

EDIT Stack Trace Below

at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed() at Microsoft.EntityFrameworkCore.DbContext.Add[TEntity](TEntity entity) at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.Add(TEntity entity) at Employer.API.Data.Repository.RepositoryBase1.Insert(TEntity entity) in C:\Information Systems\Micro Services\Employer.API\head\Employer.API.Data\Repository\RepositoryBase.cs:line 29 at Employer.API.Business.DataProvider.EmployerCustomDataProvider.d__5.MoveNext() in C:\Information Systems\Micro Services\Employer.API\head\Employer.API.Business\DataProvider\EmployerCustomDataProvider.cs:line 77 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Audit.Core.AuditScope.<SaveEventAsync>d__40.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Audit.Core.AuditScope.<SaveAsync>d__34.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Audit.EntityFramework.DbContextHelper.<SaveScopeAsync>d__21.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Audit.EntityFramework.DbContextHelper.<SaveChangesAsync>d__34.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Audit.EntityFramework.AuditDbContext.d__36.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Employer.API.Data.UnitOfWork.UnitOfWork.<CommitAsync>d__48.MoveNext() in C:\Information Systems\Micro Services\Employer.API\head\Employer.API.Data\UnitOfWork\UnitOfWork.cs:line 103 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Employer.API.Business.Services.EmployerBranchService.<CreateUpdateEmployerBranch>d__6.MoveNext() in C:\Information Systems\Micro Services\Employer.API\head\Employer.API.Business\Services\EmployerBranchService.cs:line 135 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Employer.API.Business.Services.EmployerBranchService.d__5.MoveNext() in C:\Information Systems\Micro Services\Employer.API\head\Employer.API.Business\Services\EmployerBranchService.cs:line 89

0

There are 0 best solutions below