Ali.M Eghbaldar
Ali.M Eghbaldar

Reputation: 1

How to access a facade interface through DbContext?

I have a tough question, it'd be great if anyone helped me to solve that. There is a facade class for modifiying entities and I want to get users' actions when a acntion happens and store that in UsersActionsLog entity:

 public class UsersActionsLog
    {
        [Key]
        public long Id { get; set; }
        public long UserId { get; set; }
        public string Entity { get; set; }
        public string? PrimaryKeyValue { get; set; }
        public string PropertyName { get; set; }
        public string OldValue { get; set; }
        public string NewValue { get; set; }
        public string Action { get; set; } // [0]=Added [1]=Modified [2]=Deleted
        public bool Successful { get; set; } // [false]= failed [true]=successful
        public DateTime Date { get; set; } = DateTime.Now;
    }

And I am using facade pattern to execute the code. My Service:

    public class RequestPostUserActionLogServiceDto
    {
        public long UserId { get; set; }
        public string Entity { get; set; }
        public string? PrimaryKeyValue { get; set; }
        public string PropertyName { get; set; }
        public string OldValue { get; set; }
        public string NewValue { get; set; }
        public string Action { get; set; } // [0]=Added [1]=Modified [2]=Deleted
        public bool Successful { get; set; } // [false]= failed [true]=successful
    }
    public interface IPostUserActionLogService
    {
        bool Execute(RequestPostUserActionLogServiceDto req);
    }
    public class PostUserActionLogService: IPostUserActionLogService
    {
        private readonly IDataBaseContext _context;
        public PostUserActionLogService(IDataBaseContext context)
        {
            _context = context;
        }
        public bool Execute(RequestPostUserActionLogServiceDto req)
        {
            return true;
        }
    }

My facade:

 public class UserActionsLogFacade: IUserActionLogFacade
    {
        private readonly IDataBaseContext _context;
        public UserActionsLogFacade(IDataBaseContext context)
        {
            _context = context;
        }
        ///////////////////////////////////// PostUserActionLogService
        private PostUserActionLogService _postUserActionLogService;
        public PostUserActionLogService PostUserActionLogService
        {
            get { return _postUserActionLogService = _postUserActionLogService ?? new PostUserActionLogService(_context); }
        }
    }

and:

    public interface IUserActionLogFacade
    {
        public PostUserActionLogService PostUserActionLogService { get;}
    }

I am trying to inject "ISerActionLogFacade" in "DataBaseContext", but I was faced with an error [A circular dependency was detected for the service of type ] enter image description here

    public class DataBaseContext : DbContext, IDataBaseContext
    {
        private IUserActionLogFacade _userActionLogFacade;
        public DataBaseContext(DbContextOptions option, IUserActionLogFacade userActionLogFacade) : base(option)
        {
            _userActionLogFacade = userActionLogFacade;
        }

        // Tables
        public DbSet<Users> Users { get; set; } // Users Table
        public DbSet<Roles> Roles { get; set; } // Roles Table
        public DbSet<UsersInRoles> UsersInRoles { get; set; } // UsersInRoles Table
        public DbSet<UsersActionsLog> UsersActionsLog { get; set; } // UsersActionsLog Table

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //> FLuent API of Entity Configurations
            //---- Users
            modelBuilder.ApplyConfiguration(new UsersConfigurations());
            //---- ROles
            modelBuilder.ApplyConfiguration(new RolesConfigurations());
            //< End
        }
        public override int SaveChanges()
        {
            var modifiedEntries = ChangeTracker.Entries()
                .Where(
                    e =>
                    e.State == EntityState.Modified ||
                    e.State == EntityState.Added ||
                    e.State == EntityState.Deleted
                    );


            foreach (var entry in modifiedEntries)
            {
                var entityType = entry.Context.Model.FindEntityType(entry.Entity.GetType());
                var inserted = entityType.FindProperty("InsertDate");
                var updated = entityType.FindProperty("UpdateDate");
                var deleted = entityType.FindProperty("DeleteDate");

                switch (entry.State)
                {
                    case EntityState.Added:
                        if (inserted != null) entry.Property("InsertDate").CurrentValue = DateTime.Now;
                        break;
                    case EntityState.Modified:
                        if (updated != null) entry.Property("UpdateDate").CurrentValue = DateTime.Now;
                        break;
                    case EntityState.Deleted:
                        if (deleted != null) entry.Property("DeleteDate").CurrentValue = DateTime.Now;
                        entry.State = EntityState.Modified;
                        break;
                }


                _userActionLogFacade.PostUserActionLogService.Execute( new RequestPostUserActionLogServiceDto 
                {
                    Entity = entry.Entity.GetType().Name, //entityName 
                    Action = "",
                    NewValue = "",
                    OldValue = "",
                    PrimaryKeyValue = "1",
                    PropertyName = "",
                    Successful = true,
                    UserId = 1,
                });
            }
            return base.SaveChanges();
        }
    }

Of course, I saw this issue: I saw this issue: DbContext Override SaveChanges not firing but the problem is, based on that issue the answerer has solved the problem directly. I mean we must not use the DOMIAN layer directly in the PERSISTENCE layer because of the CLEAN ARCHITATURE structure. So, how can I inject the facade and use its service in the OVERRIED SAVECHANGE() method?

Upvotes: 0

Views: 99

Answers (1)

Steve Py
Steve Py

Reputation: 34908

When it comes to things like auditing and error logging to DB, the solution I use is a bounded DbContext. Basically for your auditing facade, don't use the DataBaseContext, but instead create something like an RequestActionDbContext which defines just this RequestAction entity and is injected into the DataBaseContext to be called on an intercepted SaveChanges(). This avoids the circular dependency issues and ensures that saving a RequestAction with a SaveChanges() doesn't trigger an infinite recursion stack overflow.

The downside of this approach is that the two operations, saving the entity being tracked, and saving the audit record(s) are independent so if one fails, the other doesn't roll back. So if that is important then it needs to be coordinated manually. Typically I would set up the facade DTO(s) during the SaveChanges() as-per normal but then commit them to the Facade (to be written using the RequestActionDbContext after calling base.SaveChanges():

var result = base.SaveChanges();
try
{
    _userActionLogFacade.PostUserActionLogService.Execute(requestActionDto);
}
catch (Exception ex)
{
     // Log exception... Save succeeded but audit failed...
}
return result;

It's not perfect, but IMO covers the 99.95%. If it were to fail with the audit call something serious or freakish has happened and I would want to investigate and handle that scenario manually.

Upvotes: 0

Related Questions