0

I'm developing a web app using ASP.NET Core + Entity Framework Core.

I'd like to store more info regarding creation, modification & deletion.

My EF entities implement interfaces which bring the following fields:

  • DateTime CreationTime { get; set; }
  • long? CreatorUserId { get; set; }
  • DateTime? ModificationTime { get; set; }
  • long? ModifierUserId { get; set; }
  • DateTime? DeletionTime { get; set; }
  • public long? DeleterUserId { get; set; }
  • bool IsDeleted { get; set; }
  • int TenantId { get; set; }

I've been trying to make my app set all of the fields automatically.

Right now I am only able to handle the DateTime fields:

AppDbContext.cs

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
    ChangeTracker.DetectChanges();
    ChangeTracker.ProcessModifiedTime();
    ChangeTracker.ProcessSoftDelete();
    ChangeTracker.ProcessCreationTime();

    return (await base.SaveChangesAsync(true, cancellationToken));
}

ChangeTrackerExtensions.cs

public static class ChangeTrackerExtensions
{
    public static void ProcessCreationTime(this ChangeTracker changeTracker)
    {
        foreach (var item in changeTracker.Entries<ICreationTime>().Where(e => e.State == EntityState.Added))
        {
            item.CurrentValues[AppConsts.CreationTime] = DateTime.Now;
        }
    }

    public static void ProcessModifiedTime(this ChangeTracker changeTracker)
    {
        foreach (var item in changeTracker.Entries<IModificationTime>().Where(e => e.State == EntityState.Modified))
        {
            item.CurrentValues[AppConsts.ModificationTime] = DateTime.Now;
        }
    }

    public static void ProcessSoftDelete(this ChangeTracker changeTracker)
    {
        foreach (var item in changeTracker.Entries<ISoftDelete>().Where(e => e.State == EntityState.Deleted))
        {
            item.State = EntityState.Modified;
            item.CurrentValues[AppConsts.IsDeleted] = true;
            item.CurrentValues[AppConsts.DeletionTime] = DateTime.Now;
        }
    }
}

At a Service Layer or Controller level it is possible to assign the required value each time (CreatorUserId, MofidierUserId, DeleterUserId) but this becomes a routine task, very tedious.

I've seen the AspNetBoilerplate implementation, but what worries me about it is that the developers make their DbContext dependent on Session and other stuff.

public abstract class AbpDbContext : DbContext, ITransientDependency, IShouldInitialize
{ 
    public IAbpSession AbpSession { get; set; }
    public IEntityChangeEventHelper EntityChangeEventHelper { get; set; }
    public ILogger Logger { get; set; }
    public IEventBus EventBus { get; set; }
    public IGuidGenerator GuidGenerator { get; set; }
    public ICurrentUnitOfWorkProvider CurrentUnitOfWorkProvider { get; set; }
    public IMultiTenancyConfig MultiTenancyConfig { get; set; }
    ...

IMHO, this is a typical violation of Single Responsibility Principle.

I tried looking into interceptors, but they seem to be available only in ordinary (non-core) edition of Entity Framework - still haven't managed to get my head around how to implement what I mentioned above.

Really, I'm completely lost as to how to handle it with Entity Framework Core. Can anyone think of any Service Layer level solution for that? Or maybe there's a better way?

Alex Herman
  • 2,708
  • 4
  • 32
  • 53

1 Answers1

1

UserId and TenantId here are just pieces of data that your repository service (here a DbContext) needs. If a service needs data, it should be injected into the service.

Without DI you might make UserID and TenantID constructor parameters on your DbContext. With DI you might inject a dependency on a service that provides this data.

David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • Just looking at the following answer https://stackoverflow.com/a/51126357/5374333 "Two notes" section, clause 2 - SRP issue. Still very confised about whether we really may inject session or any other stuff into `DbContext`. – Alex Herman Sep 01 '18 at 12:18
  • 2
    Giving your DbContext open access to the session state or the Http context is clearly wrong. But having your DbContext depend on a small number of fixed parameters is fine. Both the UserId and the TenantId might be necessary to connect to the correct database using the correct identity and record audit data. All well within the single responsibility of the repository. – David Browne - Microsoft Sep 01 '18 at 15:21
  • I introduced Session class which is registered as Scoped/Per Request, it belongs to Service Layer and gets populated in MVC Filter - we fetch UserID & TenantID from Claims, this way when we inject it further in any Service - we have UserId & TenandId and SRP is not violated. But still I fear injecting this Session instance into DbContext may be wrong in terms of SRP (even if we don't inject `HttpContext`). What do you think? Posted my workaround https://stackoverflow.com/a/52129616/5374333 – Alex Herman Sep 01 '18 at 15:34
  • 1
    Sounds good so long as you don't add a bunch of other members to the ISession service. For instance need to be able to easily create an implementation that you can use in Unit Tests or other contexts where you don't have an HTTP request, like a batch process. – David Browne - Microsoft Sep 01 '18 at 16:52
  • Sorry to interject with my own question, but I think it will be relevant to the OP as well. So, is what you are describing here a way to implement a repository-agnostic domain model? And are you saying that the way to do this is to have your repository implementation depend on an "ID provider" of sorts (the implementation of which would be in the same architectural layer)? If so, could you write a few words on how would that work when reading (and updating) data? I guess you'd have to map the IDs to the domain objects somehow, and that the whole DDD entities vs values idea becomes important. – Filip Milovanović Sep 01 '18 at 21:58
  • @FilipMilovanović I don't think that's it at all. OP's repository just needs to know the identity of the application user, and what tenant the user belongs to. We were discussing whether and how to pass this information to the repository. – David Browne - Microsoft Sep 01 '18 at 22:06
  • @DavidBrowne-Microsoft just would like to double check - would it be better to have `Session` injected into `DbContext`? Or having `UserID` & `TenantID` fields in `DbContext` is a more appropriate solution? (the problem with the latter is that we'll have to set those fields manually in any service in a place like constructor; whereas with injection of`ISession` we already have a pre-populated object which persists those values per request) – Alex Herman Sep 02 '18 at 03:08
  • especially when Session contains only those 2 fields UserID & TenantID – Alex Herman Sep 02 '18 at 03:09
  • we can also drop `ISession` and resolve `DbContext` in action filter and populate its fields - `UserID` & `TenantID`. but another problem that comes to my mind - should the `DbContext` be extended so that it also acts like object containing current session info? IMHO - this might lead to the violation of SRP. so, probably having a separate `ISession` instance carrying our session info and which can be injected into any part of the app (as well as into `DbContext`) is better? – Alex Herman Sep 02 '18 at 03:16