Announcement

Collapse
No announcement yet.

EF Core (3.0) globales save/update Event für Audit in Web-API

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • EF Core (3.0) globales save/update Event für Audit in Web-API

    Hallo Community

    Gibt es im EF Core, bei mir die Version 3, die möglichkeit bei jedem Save, oder auch Update, ein Event auszulösen?
    Ich will dazu nicht jede Entität anpassen müssen. Ich sollte bei jeder änderung eines Datensatzes den User und die Zeit im Record vermerken. Dazu müsste ich beim Event die Entität übergeben bekommen damit ich auf ein bestimmtes Interface prüfen kann.

    Ansätze dazu, auch andere Herangehensweisen?

    Gruss
    Zuletzt editiert von gfoidl; 17.10.2019, 18:23.
    Gruss
    Roland Schumacher alias GENiALi
    GENiALi's Blog

  • #2
    Hallo,

    ChangeTracker.StateChanged Event.

    andere Herangehensweisen?
    Die Datenbank das Auditing übernehmen lassen. Hat auch den Vorteil dass Ändeurngen wie z.B. via SQL Management Studio berücksichtigt werden.



    mfG Gü
    "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

    Comment


    • #3
      Woher würde die Datenbank den angemeldeten User kennen?
      Kann schlecht für jeden Web User ein DB Login erstellen lassen.
      Gruss
      Roland Schumacher alias GENiALi
      GENiALi's Blog

      Comment


      • #4
        Hallo,

        woher sollten wir auch wissen dass es um Web geht wenns in der Frage nicht erwähnt wurde?

        Edit: 17.10.2019: hab den Titel angepasst

        mfG Gü
        Zuletzt editiert von gfoidl; 17.10.2019, 18:23.
        "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

        Comment


        • #5
          Originally posted by gfoidl View Post
          woher sollten wir auch wissen dass es um Web geht wenns in der Frage nicht erwähnt wurde?
          Korrekt. :-)

          Gruss
          Roland Schumacher alias GENiALi
          GENiALi's Blog

          Comment


          • #6
            Hallo,

            kommst du mit dem ChangeTracker weiter?

            mfG Gü
            "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

            Comment


            • #7
              Kommt noch. Muss zuerst noch einwenig infrastruktur vorbereiten.
              Ein kleiner Punkt kam mir aber schon in den Sinn. Da ich mit den klasischen Schichten arbeite, API->BAL->DAL, kenne ich, glaube ich, den User auf dem DAL nicht. Den müsste ich vom API auslesen und dann runter geben.
              Ich könnte allerdings den Web Context im DAL einbinden. Aber, A) fühlt sich das falsch an und B) ob ich dann in dem Moment den richtigen User auslese da es async ist, dass weiss ich auch nicht.
              Gruss
              Roland Schumacher alias GENiALi
              GENiALi's Blog

              Comment


              • #8
                Hallo,

                Ich könnte allerdings den Web Context im DAL einbinden
                ich würde das so auch nicht machen -- da die untere Schicht (DAL) nichts vom Web Context wissen soll.

                Den müsste ich vom API auslesen und dann runter geben.
                Dieses Audit würde ich als "cross cutting concern" sowieso eher außerhalb vom API und DAL ansiedeln (auch nicht im BAL).
                Da du EF Core 3.0 verwendest, gehe ich von ASP.NET Core (3.0) aus. Stimmt das?

                Fertig gedacht hab ich die Idee nicht, aber sie wäre wie folgt:
                Die per DI registrierten Services sind alle "scoped", d.h. pro Request werden die gleichen Instanzen vom Container geholt.
                Es gibt eine Audit-Middleware, welche den DbContext injiziert bekommt, und vom ChangeTracker die Änderungen ausliest. Da es eine Middleware ist, hat sich auch Zugriff auf den HttpContext und somit auf den User.

                Somit sind alle Infos verfügbar und DAL, BAL sowie das API bleiben davon unberührt. Das ganze findet "hinter den Kulissen" statt.

                Ich baslte mal eine groben Prototypen, um zu schauen ob das auch klappen kann ;-)

                mfG Gü
                Zuletzt editiert von gfoidl; 17.10.2019, 18:18.
                "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

                Comment


                • #9
                  Hallo,

                  das geht sogar relativ einfach ;-) (Und das erste mal dass ich eine Middleware geschrieben habe, die auch Sinn macht, nicht nur so Probier-Beispiele).
                  Schau mal das angehängte Projekt an.
                  Es ist 0815 Api-Controller mit nur DAL (auch wenn ein BAL vorhanden ist, etc. so ändert das nichts).
                  Relevant ist der Middleware-Ordner (bzw. die Typen darin) und die Registrierung der Middleware im Startup (nach der Authentifizierung, sonst dürfte kein Benutzer vorhanden sein?!).

                  Klar lässt sich das noch ausbauen, aber von der Idee her sollte es (hoffentlich) rüberkommen wie ich das meine.

                  mfG Gü
                  Attached Files
                  "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

                  Comment


                  • #10
                    Originally posted by gfoidl View Post
                    Klar lässt sich das noch ausbauen, aber von der Idee her sollte es (hoffentlich) rüberkommen wie ich das meine.
                    mfG Gü
                    Hey krass....
                    Will mich Heute mal dem Problem annehmen und habe deshalb hier noch nachgelesen. Ging nicht vergessen das Thema.
                    Werde mir das also jetzt reinziehen und testen. :-) Ich geb dan Feedback.
                    Gruss
                    Roli
                    Gruss
                    Roland Schumacher alias GENiALi
                    GENiALi's Blog

                    Comment


                    • #11
                      Funktioniert soweit wirklich zufriedenstellend. Nur ein kleines Problem habe ich aktuell noch.

                      Code:
                      public async Task InvokeAsync(HttpContext httpContext, DbContext dbc)
                      {
                          dbc.ChangeTracker.StateChanged += (s, e) =>
                          {
                              if (e.Entry.Entity is ISimpleDomainObject)
                              {
                                  string userName = httpContext.User.Claims.FirstOrDefault(c => c.Type == "username")?.Value;
                      
                                  ((ISimpleDomainObject) e.Entry.Entity).ModifiedUser = userName;
                                  ((ISimpleDomainObject) e.Entry.Entity).ModifiedDate = DateTime.Now;
                                  dbc.SaveChanges();
                              }
                          };
                      
                          await _next(httpContext);
                      }
                      Das Event wird beim SaveChanges() ausgelöst. Somit drehe ich mich hier im Kreis. Mein ansatz wäre nun, Event -= machen, SaveChanges ausführen, Event += machen.
                      Gefällt mir aber nicht.
                      Gruss
                      Roland Schumacher alias GENiALi
                      GENiALi's Blog

                      Comment


                      • #12
                        Hallo,

                        falls möglich würde ich diese Audit-Info nicht im tatsächlichen Datensatz speichern, sondern in einer eigenen Audit-Tabelle. Dann kannst du via extra DbContext das sauber behandeln -- im Beispiel von mir wäre es statt [tt]PrintToConsole[/tt] dann eben das Speichern in diesem eigenen DbContext.
                        In der DB dann per Join lassen sich die Infos schön zusammenführen.

                        Das Speichern würde ich jedenfalls nach dem [tt]await _next(httpContext);[/tt] durchführen, da dort die Controller-Actions erledigt sind.
                        Ggf. dort auch das Ereignis de-registrieren.

                        httpContext.User.Claims.FirstOrDefault(c => c.Type == "username")?.Value;
                        Geht [tt]httpContext.User.Identity.Name[/tt] nicht?

                        mfG Gü
                        "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

                        Comment


                        • #13
                          Originally posted by gfoidl View Post
                          Geht [tt]httpContext.User.Identity.Name[/tt] nicht?
                          Nein. Ich habe nur den übermittelten JWT Token. Dort drin ist so einiges codiert. Unter anderem auch Claims und der Claim "username". Den nutze ich mal.

                          Gruss
                          Roland Schumacher alias GENiALi
                          GENiALi's Blog

                          Comment


                          • #14
                            Habs nun umgebaut in eine eigene Audit Tabelle. Finde ich persönlich eh besser als die angedachte Lösung, und bietet auch viel mehr möglichkeiten danach mit den Daten zu arbeiten.

                            Code:
                            public async Task InvokeAsync(HttpContext httpContext, DbContext dbContext)
                            {
                                dbContext.ChangeTracker.StateChanged += (s, e) =>
                                {
                                    if (e.Entry.Entity is ISimpleDomainObject && e.Entry.Entity is IIdentifiable)
                                    {
                                        string userName = httpContext.User.Claims.FirstOrDefault(c => c.Type == "username")?.Value;
                                        int id = ((IIdentifiable) e.Entry.Entity).Id;
                            
                                        Tracking tracking = new Tracking(id, userName, DateTime.Now, e.Entry.Entity.GetType().Name, JsonConvert.SerializeObject(e.Entry.Entity));
                                        dbContext.Tracking.Add(tracking);
                                        dbContext.SaveChangesAsync();
                                    }
                                };
                            
                                await _next(httpContext);
                            }
                            Wenn ich das await _next(httpContext); an den Anfang verschiebe wird das Event nicht mehr ausgelöst. Deshalb ist es am Ende. Dieser Code macht genau was es soll. Perfekt. Und danke für die Idee(en). :-)
                            Gruss
                            Roland Schumacher alias GENiALi
                            GENiALi's Blog

                            Comment


                            • #15
                              Hallo,

                              Wenn ich das await _next(httpContext); an den Anfang verschiebe wird das Event nicht mehr ausgelöst
                              Klar, da die Controller-Actions dann vor der Registrierung des Ereignisses passieren.

                              Ich dachte so:
                              Code:
                              public async Task InvokeAsync(HttpContext httpContext, DbContext dbContext)
                              {
                                  dbContext.ChangeTracker.StateChanged += (s, e) =>
                                  {
                                      if (e.Entry.Entity is ISimpleDomainObject && e.Entry.Entity is IIdentifiable)
                                      {
                                          string userName = httpContext.User.Claims.FirstOrDefault(c => c.Type == "username")?.Value;
                                          int id = ((IIdentifiable) e.Entry.Entity).Id;
                              
                                          Tracking tracking = new Tracking(id, userName, DateTime.Now, e.Entry.Entity.GetType().Name, JsonConvert.SerializeObject(e.Entry.Entity));
                                          dbContext.Tracking.Add(tracking);
                                      }
                                  };
                              
                                  await _next(httpContext);
                              
                                  await dbContext.SaveChangesAsync();
                              }
                              D.h. dass nach den Controller-Actions, wenns sonst nichts mehr mit dem DbContext passiert, wird das Tracking gespeichert.
                              Dein Code "erwartet" das SaveChangesAsync auch nicht, daher ist es eher ein Glücksfall wenns funktioniert (könnte passieren dass der DbContext disposed / in den Pool zurückgesetzt wird, bevor das Speichern erledigt wird -- eine "klassische" Race-Situation).

                              mfG Gü
                              "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". - Martin Fowler

                              Comment

                              Working...
                              X