How To Track Entity Changes With EF Core | Audit Logging

Поділитися
Вставка
  • Опубліковано 11 січ 2025

КОМЕНТАРІ • 117

  • @MilanJovanovicTech
    @MilanJovanovicTech  2 роки тому +9

    Want to master Clean Architecture? Go here: bit.ly/3PupkOJ
    Want to unlock Modular Monoliths? Go here: bit.ly/3SXlzSt

  • @williamliao4399
    @williamliao4399 2 роки тому +13

    a quick reminder from the case I encountered, there are two methods looks very similar in SaveChangesInterceptor, one is called SavingChangesAsync and one is call SavedChangesAsync, in thevideo we are implementing SavingChangesAsync. sometimes your IDE will aurocomplete to the other one. make sure you are overriding the correct method otherwise it won't work

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +2

      Thanks for pointing that out, very important

    • @mohammadumar443
      @mohammadumar443 4 місяці тому

      @@MilanJovanovicTech above video is part of which course or youtube series ? I want complete playlist.

  • @pilotboba
    @pilotboba 2 роки тому +4

    This is populating some audit fields but you still only have the latest state of the entity. There is no capture here of the values that were changed. Yes, I know when a change was made, and you can also include the user to know who changed it. But you don't know what was changed. This may be fine for some use cases.
    One option to get a real audit log with changes is to use TemporalTables if you are in SQL server.
    Or, if you really want a more robust method take a look at the EventSourcing pattern.

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      Absolutely Bob, you are spot on with Temporal Tables/Event Sourcing if we need more details about _what_ is changed.

  • @CRBarchager
    @CRBarchager 2 роки тому +3

    4:30 Was that some sort of shortcut you made there to create the foreach that fast? - Or was it the editing of the video that made it look like magic?

  • @vprix2000
    @vprix2000 Рік тому +2

    Thank you for the tutorial, very useful.
    There is something that I have noticed: the State is always Unchanged in the after change listener. that feels like a very important oversight given it will never tell you what happened.
    There is always also the issue that EF Core does not have events for delete. 🤦‍♂

    • @MilanJovanovicTech
      @MilanJovanovicTech  Рік тому

      Not entirely sure what you mean? 🤔

    • @vprix2000
      @vprix2000 Рік тому

      @@MilanJovanovicTech in the SavedChangesAsync (or SavedChanges) method of the interceptor there is no way to know if the entity was inserted or updated (retrieving the entity from the ChangeTracker and asking for the state, it is marked as Unchanged, even if it was just inserted into the database).

  • @bilalmehrban
    @bilalmehrban 2 роки тому +3

    I see, seems interesting. Currently I am overriding the savechangesasync method but it has some issues in my use cases. This seems to resolve those definately will give it try :) Thanks :)

  • @KenzoArts
    @KenzoArts Рік тому +1

    Nice feature indeed !
    I am wondering, what are pros and cons of Auditing
    using Temporal tables in SQL Server vs Auditing using Interceptors ?
    Thanks Milan

    • @MilanJovanovicTech
      @MilanJovanovicTech  Рік тому

      More memory usage, a bit more difficult to query? 🤔
      Less complicated to set up.

  • @elpe21
    @elpe21 2 роки тому +3

    There is 'Entity' property on EntityEntry through which you can access properties directly. Also the described method won't work if we want to keep track of IDs generated by the database on insert.

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +1

      The properties have a private setter, so that would't work with my implementation

    • @321zipzapzoom
      @321zipzapzoom Рік тому

      Hi,@@MilanJovanovicTech I able to use -Entity' Property la tell what is difference using auditableEntity.Property(x => x and auditableEntity.Entity. Thanks

  • @jolambrichts
    @jolambrichts 2 роки тому +1

    Thanks, nice video. I think VS2022 has also the possibility to cleanup unused namespaces on file save

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      Could be, I didn't hear of that before. I tend to use default settings in general.

  • @sergiom.954
    @sergiom.954 2 роки тому +1

    Very useful video, thanks very much!

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      I'm glad you liked it. Did you have to build something similar before?

  • @androidsavior
    @androidsavior Рік тому +1

    Did you share the full code of this project ? I want to learn clean arc. From it

  • @thematthewyoung
    @thematthewyoung 2 роки тому +1

    Do you have a video on how you set up your visual studio? I love the way your Intellisense looks, and your color scheme. It looks like default with just a few nice modifications.

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      That's coming from ReSharper mostly. I use the R# Dark theme

    • @thematthewyoung
      @thematthewyoung 2 роки тому +1

      @@MilanJovanovicTech Thanks man! I'm currently using your implementation of MediatR and Fluent Validation to redo our template for new projects at work. the old template is cqrs done custom and poorly. excited to use packages that my junior devs can read on and move our custom code to best practices and set a standard. became a patron to get that source code thank you so much man!

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      @@thematthewyoung I'm glad you found some value out of it. And I appreciate the support :)
      That's amazing that you're taking steps to improve the project template. I'm sure it will pay off.

  • @kodindoyannick5328
    @kodindoyannick5328 11 місяців тому

    Thank you for the tutorial, very useful.

  • @mladenstankovic2428
    @mladenstankovic2428 2 роки тому +2

    Opinions on using this approach vs having DB triggers for Insert/Update/Delete operations?

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +2

      DB triggers are an excellent option, although they will slow down the DB in high load scenarios? Doing it in application seems simpler to me.
      But there's a slight issue. Let's say you also want to log the ID of the User who made the change. You can't achieve that via triggers, but you can do that with interceptors.

  • @techfamily401
    @techfamily401 Рік тому +1

    Good video, many thanks

  • @gerarduab9960
    @gerarduab9960 Рік тому +1

    Can the interceptor have a Principal in orher to obtain curren claims of user?

    • @MilanJovanovicTech
      @MilanJovanovicTech  Рік тому

      Yes, you can probably do that with IHttpContextAccessor. Note that the HttpContext will be null for server-only scenarios.

  • @johncerpa3782
    @johncerpa3782 2 роки тому +1

    Excellent, thank you

  • @emwagner
    @emwagner 2 роки тому +2

    Milan, thanks for the awesome videos. What if you wanted to track the table columns affected, their previous values before CRUD and the new values after CRUD?

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      That's an entirely different thing, you need some sort of history pattern. Check out temporal tables with SQL Server

    • @emwagner
      @emwagner 2 роки тому

      @@MilanJovanovicTech I've done this in .NET 5 many times using auditing setup in MVC. Was just curious if v6 and v7 had the same capability or different coding structure. The coding habits usually change, sometimes drastically when a new SDK version is released.

  • @jmvd_uy8639
    @jmvd_uy8639 Рік тому +1

    Hi Milan, something I'd like to understand is where would that information (CreatedOnUtc, ModifiedOnUtic) be stored. Would it be stored on a separate table in the database?

  • @02244
    @02244 2 роки тому +1

    What if you need to use the base SaveChanges method in certain cases? How do I disable the interceptor? In case with overriding SaveChanges it's easy.

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +2

      How do you disable any middleware?
      You introduce a switch of some sorts that you can control, and turn on/off as you wish.

    • @dhmilmile1
      @dhmilmile1 2 роки тому +1

      In that case override save changes and save changes async method in db context class and move the entity and entry check logic there. I prefer the override method cause I used save changes frequently.

  • @anonim9783
    @anonim9783 Рік тому +1

    How would you update in this approach if the interface has a user update field?

    • @MilanJovanovicTech
      @MilanJovanovicTech  Рік тому +1

      Find a way to inject the current user ID and save that

    • @anonim9783
      @anonim9783 Рік тому

      @@MilanJovanovicTech Yep exactly , I was wondering In clean architecture 'domain' not have knowledge about currentUser, or embed this logic somewhere else

  • @shkelqimhaxha3985
    @shkelqimhaxha3985 2 роки тому +4

    Hey Milan, great video. I have already implemented this kind of audit using EFCore, but I have found one setback in this. If you have related entities in your model, and for some reason it happens that the related entities are modified, then this way of audit will not work because EFCore change tracker doesn't track related entities (as per my experience). Have you found a workaround on this ?

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому

      You can always traverse down the navigation properties using the EntityEntry. But do you want to audit navigation properties if they weren't changed?

    • @shkelqimhaxha3985
      @shkelqimhaxha3985 2 роки тому +1

      ​​ @Milan Jovanović In fact I think I was a bit unclear on my question. I was trying to keep track of my entities using 'CurrentValue' and 'OriginalValue' properties. While EFCore tracks these properties in an entity, it does not work with related entities. I also tried traversing down related entities in different ways but could not get its 'OriginalValue' property. Anyway here's a picture of how I implemented logs in EF while overriding savechangesasync method. The only problem with this approach is that I cannot track changes in related entities i.imgur.com/oGH9ubs.png

    • @pilotboba
      @pilotboba 2 роки тому

      @@shkelqimhaxha3985 If you are using SQL server, take a look at temporal tables. SQL does most of the work for you, and EF core (at least 6) supports it.

    • @ruandias6257
      @ruandias6257 Рік тому

      I'm with the same problem, did you find a solution to get previous value and current value to related entities using entity framework ?@@shkelqimhaxha3985

  • @microtech2448
    @microtech2448 5 місяців тому

    How do you recommend to pass logged in user id to log who added/ changed the record ?

    • @MilanJovanovicTech
      @MilanJovanovicTech  5 місяців тому

      - Grab it from the HttpContext, which means we can only do this in an API request
      - Or you can include the UserId as part of the incoming request

    • @microtech2448
      @microtech2448 5 місяців тому

      @@MilanJovanovicTech Hmm that's what I thought. Thanks for sharing your views, we seem on the same page 🙂

  • @baolee4622
    @baolee4622 2 роки тому +1

    I can do all this in the context file without register any services. Which one is better?

  • @kuba89089
    @kuba89089 2 роки тому +2

    Can you share a sample project with good DDD/CQRS pratices?

  • @rahulanrajasekaram4366
    @rahulanrajasekaram4366 9 місяців тому

    Thanks for the useful video! Can you please share some knowledge about how to resolve scoped dependencies within the Interceptor? E.g IHttpContextAccessor? Could not find any working examples :(

    • @MilanJovanovicTech
      @MilanJovanovicTech  9 місяців тому

      IHttpContextAccessor is a singleton. You can inject it without a problem. The question is will the HttpContext property be null or not :)

  • @birukayalew3862
    @birukayalew3862 2 роки тому +1

    simple but useful 👍

  • @novaploca2080
    @novaploca2080 2 роки тому +1

    Using this implementation only last modified date will be written in db?

  • @fernandocalmet
    @fernandocalmet 2 роки тому +1

    Milan, is it possible to capture here the ID of the person who made the modification or creation to save not only the date but also who made the transaction?

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +4

      You would need to inject a service that provides that ID. Since the interceptor is a Singleton service, this will probably be challenging. So another option would be to move this into DbContext.SaveChangesAsync. Since DbContext is scoped, you can inject a service like IHttpContextAccessor. What do you think?

    • @fernandocalmet
      @fernandocalmet 2 роки тому

      @@MilanJovanovicTech Interesting, I'm going to try it and I'll tell you later how it went, thanks Milan😃

    • @abdulnaserramadan37
      @abdulnaserramadan37 2 роки тому

      I am using the below code in UnitOfWork:
      public async Task Save(HttpContext httpContext)
      {
      var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
      var user = await _userManager.FindByIdAsync(userId);
      var entries = _context.ChangeTracker.Entries()
      .Where(q => q.State == EntityState.Modified || q.State == EntityState.Added);
      foreach (var entry in entries)
      {
      if (entry.State == EntityState.Added)
      {
      ((EntityBase)entry.Entity).CreatedBy = user.UserName;
      }
      if (entry.State == EntityState.Modified)
      {
      ((EntityBase)entry.Entity).UpdatingDate = DateTime.UtcNow;
      ((EntityBase)entry.Entity).UpdatedBy = user.UserName;
      }
      }
      await _context.SaveChangesAsync();
      }
      And The EntityBase:
      public abstract class EntityBase
      {
      public EntityBase()
      {
      CreationDate = DateTime.UtcNow;
      }
      [Key]
      public int Id { get; set; }
      public string CreatedBy { get; set; } = string.Empty;
      public string UpdatedBy { get; set; } = string.Empty;
      public DateTime CreationDate { get; set; }
      public DateTime UpdatingDate { get; set; }
      [Timestamp]
      public byte[] TimeStamp { get; set; }
      }

    • @purplepiranha
      @purplepiranha 4 місяці тому

      @@MilanJovanovicTech why not register the interceptor as a scoped service? Is there some underlying reason that we shouldn't?
      Edit: We cannot inject into an interceptor as it prevents us from scaffolding migrations, so using a scoped service was pointless.
      I did find out however that the solution's a lot easier than I thought.
      The DBContext has a GetService method that will expose the service from the DB Context's scope.

  • @Tamer_Ali
    @Tamer_Ali 2 роки тому +1

    Thanks Milan for your awesome video 👍
    Is there a relation between Notification "e.g: like what SignalR do" and CQRS?
    if yes, I hope you explain it in one of your upcoming videos
    if no, I hope you also explain how to use it

  • @rktmimob
    @rktmimob 2 роки тому +1

    Hi Milan,
    Thanks for this.
    is their any major performance impact if we follow this method for auditing or any other better way ?

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +1

      No, you won't notice anything as it's all happening in-memory

  • @sunilanthony17
    @sunilanthony17 2 роки тому +1

    Would this slow down your performance of the application?

  • @rubiglam
    @rubiglam 11 місяців тому

    Nice video. Why choose DateTime instead of DateTimeOffset?

  • @JoaoRoberto-mm4qj
    @JoaoRoberto-mm4qj Рік тому +1

    Sorry I couldnt get how you saved this audit in database, if you saved for exemple “old values” and “new values”

  • @kondziossj3
    @kondziossj3 2 роки тому +2

    Is It possible to recaive link to github for that code?

  • @sajadmalik9097
    @sajadmalik9097 4 місяці тому

    I know this is an old video but please respond..
    Does it cost performance. Because now the saveChanges method goes through interceptor which will slow down the performance, right?

    • @MilanJovanovicTech
      @MilanJovanovicTech  4 місяці тому

      Well, did you try measuring this?

    • @sajadmalik9097
      @sajadmalik9097 4 місяці тому

      @@MilanJovanovicTech not yet, I just wanted to know directly..
      I might measure later.. but I haven't yet

  • @hemantpanchal8087
    @hemantpanchal8087 11 місяців тому

    Hello @Milan i came here from your linkedin profile..
    I want to know what is best way for audit trail logging when you are using .net core.
    I have huge logging mechanism. But want to know best practices and tools for same.

  • @aah134-K
    @aah134-K 2 роки тому +1

    I thought you are tracking who made what changes with the dates.
    Like when user add or delete anything entities get audited along with user info

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +1

      That would be significantly more complex. We could use temporal tables, or manually create history tables.

  • @hamzehhanandeh3647
    @hamzehhanandeh3647 10 місяців тому +1

    nice, thank you

  • @davearkley7014
    @davearkley7014 2 роки тому +1

    How would one pass a username to this interceptor so that a name can be audited

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +3

      You can inject an IServiceProvider, create a scope, and resolve IHttpContextAccessor.

    • @shkelqimhaxha3985
      @shkelqimhaxha3985 2 роки тому

      @@MilanJovanovicTech But if you work on a clean architecture type of project and this interceptor will probably be in the infrastructure (persistence) layer, then how do you do in this case ? Assuming that IHttpContextAccessor should only be injected in application or presentation layer

    • @qwertyqwerty7072
      @qwertyqwerty7072 2 роки тому +1

      @@shkelqimhaxha3985 I usually use this approach: in asp.net middleware create some sort of user context and fill it with data you need. Then store it within some service in DI (you will definitely need AsyncLocal or something like this to do that) and inject it where required

    • @rahulanrajasekaram4366
      @rahulanrajasekaram4366 9 місяців тому

      @@MilanJovanovicTech will there be a performance cost when creating a scope and resolving services?

  • @farhad_mirshekar
    @farhad_mirshekar Рік тому +1

    Great thanks a lot

  • @unskeptable
    @unskeptable 2 роки тому +2

    I see .net video, I like

  • @danielgoldberg7473
    @danielgoldberg7473 Рік тому +1

    great video! funny note, it looks like you edited out all of your blinks lmao

  • @coolfyb
    @coolfyb Рік тому

    VS theme please?

  • @nickadams2361
    @nickadams2361 Рік тому

    bro is the ms docs

  • @ChrisWard74
    @ChrisWard74 6 місяців тому

    Sorry I don't consider this Auditing, it is only tracking the last modified date/time not all of the times it's modified and it's not tracking what has modified and the values of the before and after the modification. Also while you talked about keeping track of who created/modified the record you didn't give an example of that only the date/time

    • @MilanJovanovicTech
      @MilanJovanovicTech  6 місяців тому

      You can extend the example, since the ChangeTracker gives you the CurrentValue and OriginalValue of the entity to set the before/after state in the database. Or we could explore temporal tables.

  • @sebastianwhiffen
    @sebastianwhiffen Рік тому

    bro please blink

    • @MilanJovanovicTech
      @MilanJovanovicTech  Рік тому

      I ended up rate limiting my blinks. Sucks :(

    • @sebastianwhiffen
      @sebastianwhiffen Рік тому

      @@MilanJovanovicTech might want to invest in a better blink server if you’re hitting max blinks

  • @porcinetdu6944
    @porcinetdu6944 2 роки тому +1

    Why using « entre.Proprety.(e => e.dummyField).CurrentValue » ? I never really think about and always used « entry.Entity.dummyField ». Does it make any difference ?

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 роки тому +1

      I turn on nullable-reference types feature, so I get a compile error if I don't use '?'