Coder Social home page Coder Social logo

Comments (13)

thepirat000 avatar thepirat000 commented on July 22, 2024 1

I didn't find any official documentation stating so, but I think BulkUpdate cannot be intercepted in any way.

https://www.milanjovanovic.tech/blog/how-to-use-the-new-bulk-update-feature-in-ef-core-7

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

When using SaveChanges override method without inheritance, you have to make sure you are overriding the following two methods in your DbContext:

int SaveChanges(bool acceptAllChangesOnSuccess)
Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken))

Just so you know, no other SaveChanges override is needed, since the other overloads will call one of these two.

It looks like you are not overriding the first one, so maybe the client that is not triggering the audit is calling the int SaveChanges(bool acceptAllChangesOnSuccess)

Another option is to use a SaveChanges interceptor instead of overriding the SaveChanges:

https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.EntityFramework/README.md#3-with-the-provided-save-changes-interceptor

from audit.net.

shubhamCedargate avatar shubhamCedargate commented on July 22, 2024

Hello!,
I Changed the Context class and override the two methods that you mentioned.
I also tried using the SaveChanges interceptors.
Both didn't work and the changes are not audited still.

When I put a debug point on the overridden method,
_auditContext is populated with correct values, but still, no audit events are triggered.

image

On application startup,
If I put a debug point on the Audit core configs,
that is reached as well
image

I have other projects that use the same Persistance package for database operations,
Audit events are being triggerres successfully from those projects though.
image

I have the same Audit configurations in all of these projects.
Its not working on this particular project only

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

Are you calling Audit.Core.Configuration.Setup() from all of your runnable projects? for example, if you want to audit from an ASP.NET app and a console app, you need to set up the data provider on both.

Can you share a minimal solution that reproduces the issue?

from audit.net.

shubhamCedargate avatar shubhamCedargate commented on July 22, 2024

It seems I found out why Audit events were not being triggered

I am using the following method to Bulk-Update am Entity list

    public async Task BulkUpdatePatients(string connectionString, List<Patient> patients)
    {
        var _db = new customContext(connectionString);
        _db .BulkUpdate(patients);
        foreach (var patient in patients)
        {
            var patientSources = patient.PatientSources;
            _db .BulkUpdate(patientSources.ToList());
        }
        await _db .SaveChangesAsync();
    }

When saving in the database this way, Audit events are not getting triggered.

But if I add the following in the method body,

            `patient.SOME_Column = "String_value";
            _db.Patients.Update(patient);
            `

The audit event is triggered.

    public async Task BulkUpdatePatients(string connectionString, List<Patient> patients)
    {
        var _db = new customContext(connectionString);
        _db .BulkUpdate(patients);
        foreach (var patient in patients)
        {
           patient.SOME_Column = "String_value";
            _db.Patients.Update(patient);
            var patientSources = patient.PatientSources;
            _db .BulkUpdate(patientSources.ToList());
        }
        await _db .SaveChangesAsync();
    }

How can I make it so that doing Bulk-Updates also trigger the Audit Events.

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

The Bulk updates and deletes using ExecuteUpdate() and ExecuteDelete() are executed directly to the database, without making use of the EF's change tracker, so the SaveChanges interception will not get those events.

Check this: https://learn.microsoft.com/en-us/ef/core/saving/execute-insert-update-delete#change-tracking

The only way would be to add the AuditCommandInterceptor, which will generate logs like:

{
  "CommandEvent": {
    "Method": 0,
    "CommandType": 1,
    "CommandText": "DELETE TOP(@__p_0) FROM [v]\r\nFROM [Values] AS [v]",
    "Result": 1,
    "Database": "Test_1",
    "ConnectionId": "3001fe63-b822-4422-b59e-7b941a05b2ad",
    "DbConnectionId": "33e15718-7c07-45d8-953c-acea98a0a0a8",
    "ContextId": "b72a194f-7b66-4fe2-8a4d-5e178d0edb7d:0",
    "IsAsync": false,
    "Success": true
  },
  "EventType": "EF",
  ...

from audit.net.

shubhamCedargate avatar shubhamCedargate commented on July 22, 2024

I am not getting what changed in the Logs

The generated JSON looks like this:

{"commandEvent":{"method":2, "commandType":1, "commandText":
"SELECT [p].[Id], [p].[Address_Address1],. . .
[p1].[Patient_Id], [p1].[Type]\r\nFROM [Patients] AS [p]\r\nLEFT
JOIN
(\r\n
SELECT [p0].[Id]
,[p0].[DataSourceId]
,[p0].[EmployerId]
,[p0].[LastSourceOfModification]
,[p0].[LocationId]
,[p0].[PatientId]
,. . .
,[d].[autoCreateReferralAppts]\r\n
FROM [PatientDataSources] AS [p0]\r\n
LEFT JOIN [DataSources] AS [d]
ON [p0].[DataSourceId] = [d].[Id]\r\n
WHERE [d].[AnalyticsIntegrated] = CAST(1 AS bit)\r\n
) AS [t]
ON [p].[Id] = [t].[PatientId]\r\nLEFT
JOIN [PhoneNumbers] AS [p1]
ON [p].[Id] = [p1].[Patient_Id]\r\nWHERE [p].[EnterpriseId] IN (. . .)
\r\nORDER BY [p].[Id], [t].[Id], [t].[Id0]", "
database":"888", "connectionId":"b1a843bf-b4ad-442b-9007-0basdasdads", "dbConnectionId":"asdasdasd-aefa-4103-8ac7-86e3bf6f7727", "contextId":"asdadasda-1cac-4d3e-9ae6-e121ad6d4275:0", "isAsync":true},
"eventType":"ExecuteReader", "environment":{"userName":"Shubham.Pradhan", "machineName":"CGNPLAP586", "domainName":"corp", "callingMethodName":"System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start()", "assemblyName":"System.Private.CoreLib, Version = 6.0.0.0, Culture = neutral, PublicKeyToken = sdasdadasd", "culture":"en-US", "customFields":{}}, "customFields":{"user":"API"}, "startDate":"2023-06-22T05:14:29.3827405Z", "duration":0}

The command text has a "SELECT" statement after performing BulkUpdate.

Ive placed the AuditCommandInterceptor config inside DbContext configuration:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new AuditCommandInterceptor());
}

from audit.net.

shubhamCedargate avatar shubhamCedargate commented on July 22, 2024

Hello!

I am facing another issue here
i am getting the old value and new value exactly the same even when there is an update.

My Updating method looks like this:

    public async Task BulkUpdatePatientsAndPatientDataSource(string connectionString, List<Patient> patients)
    {
        Context _db = new MyContext(connectionString);
        _db.Patients.UpdateRange(patients);
        foreach (Patient patient in patients)
        {
            ICollection<PatientDataSource> patientDataSources = patient.PatientDataSources;
            _db.PatientDataSources.UpdateRange(patientDataSources);
        }

        await _db.SaveChangesAsync();
    }

I called this method like this :

            await _patientDemographicsRepository.BulkUpdatePatientsAndPatientDataSource(clientCredential.ConnectionString, patients);

here i passed the list of updated patients

which i got by creating a new instance of the MyContext like this:

MyContext _db = new MyContext(connectionString);

and then modifying a fey patient entities.

Why am I getting the old value and new value exactly the same even when there is an update.

Is there a way I get the correct change records?

The Audit Json looks like this

{
    "entityFrameworkEvent": {
        "database": "DB001",
        "connectionId": "aaaabbbb",
        "contextId": "aaaabbbb",
        "entries": [
            {
                "table": "Patients",
                "name": "Patient",
                "primaryKey": {
                    "id": "ididididididididididididididid"
                },
                "action": "Update",
                "changes": [
                    {
                        "columnName": "Address_Address1Decrypted",
                        "originalValue": "KAthmandu",
                        "newValue": "KAthmandu"
                    },
                    {
                        "columnName": "Name_FirstNameDecrypted",
                        "originalValue": "SuPr",
                        "newValue": "SuPr"
                    },
                    {
                        "columnName": "Name_LastNameDecrypted",
                        "originalValue": "Awesome",
                        "newValue": "Awesome"
                    },
                    {
                        "columnName": "Name_MiddleName",
                        "originalValue": "",
                        "newValue": ""
                    },
                    {
                        "columnName": "Name_MiddleNameDecrypted",
                        "originalValue": "",
                        "newValue": ""
                    }
                ],
                "columnValues": {
                    "id": "ididididididididididididididid",
                    "address_Address1Decrypted": "KAthmandu",
                    "name_FirstNameDecrypted": "SuPr",
                    "name_LastNameDecrypted": "Awesome",
                    "name_MiddleName": null,
                    "name_MiddleNameDecrypted": null
                },
                "valid": true,
                "customFields": {}
            }
        ],
        "result": 24,
        "success": true,
        "customFields": {}
    },
    "eventType": "EF",
    "environment": {
        "userName": "Shubham.Pradhan",
        "machineName": "LAPTOP",
        "domainName": "corp",
        "callingMethodName": "System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start()",
        "assemblyName": "System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=aabbasd",
        "culture": "en-US",
        "customFields": {}
    },
    "customFields": {
        "user": null,
        "environment": "DEV"
    },
    "startDate": "2023-07-06T14:09:22.1093073Z",
    "endDate": "2023-07-06T14:09:22.4538774Z",
    "duration": 345
}

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

Doing the update with Update / UpdateRange will not retrieve the old values from the database, that's the way EF Change Tracker works.

From EF documentation here "Note that whenever real original property values are not available (e.g. entity was not yet persisted to the database) this will default to the current property values of this entity."

Please check this: #53 (comment)

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

I'm thinking that the Audit.EF library could provide an optional mechanism to load the original values before any modification, to cover the use of Update / UpdateRange (and also Remove and manually attached entities). This mechanism could use the GetDatabaseValues() function.

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.entityentry.getdatabasevalues?view=efcore-7.0#microsoft-entityframeworkcore-changetracking-entityentry-getdatabasevalues

Will take a deeper look

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

Starting from version 21.0.2, the Audit.EntityFramework and Audit.EntityFramework.Core libraries introduce a new feature.

The setting called ReloadDatabaseValues can be globally or individually configured for a DbContext instance. It determines whether the original values of audited entities should be fetched from the database prior to saving the audit event.

Consider the following examples of update and delete operations:

using (var context = new MyAuditedContext())
{
    //context.ReloadDatabaseValues = true;

    context.Cars.Update(new Car() { Id = 123, Name = "New name" });
    await context.SaveChangesAsync();
}
using (var context = new MyAuditedContext())
{
    //context.ReloadDatabaseValues = true;

    context.Entry(new Car() { Id = 123 }).State = EntityState.Deleted;
    await context.SaveChangesAsync();
}

When doing modifications like these, the EF Change Tracker will lack knowledge of the original values.

Enabling the ReloadDatabaseValues setting triggers an extra database query to retrieve the original values prior to the update operation. As a result, the audit event will contain the original values.

from audit.net.

shubhamCedargate avatar shubhamCedargate commented on July 22, 2024

Hello!
This solution seems to be working when I inherit the AuditDbContext class

[AuditDbContext(Mode = AuditOptionMode.OptIn, IncludeEntityObjects = false, AuditEventType = "EF")]
public class MyContext : AuditDbContext
{

and set the ReloadDatabaseValues when creating the db context

public async Task BulkUpdatePatientsAndPatientDataSource(string connectionString, List<Patient> patients)
{
    Context _db = new MyContext(connectionString);
    _dbCW.ReloadDatabaseValues = true;
    _db.Patients.UpdateRange(patients);
    foreach (Patient patient in patients)
    {
        ICollection<PatientDataSource> patientDataSources = patient.PatientDataSources;
        _db.PatientDataSources.UpdateRange(patientDataSources);
    }
    await _db.SaveChangesAsync();
}

But this is not working when I try and configure it Globally for the DbContext

[AuditDbContext(Mode = AuditOptionMode.OptIn, IncludeEntityObjects = false, AuditEventType = "EF")]
public class MyContext : DbContext
{
    public MyContext(string connectionString) : base(GetOptions(connectionString))
    {
        _auditContext = new DefaultAuditContext(this);
        _auditContext.ReloadDatabaseValues = true;
        _helper.SetConfig(_auditContext);
    }
    
    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
         return await _helper.SaveChangesAsync(_auditContext, () => base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken));
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        return _helper.SaveChanges(_auditContext, () => base.SaveChanges(acceptAllChangesOnSuccess));
    }
}

Is this the correct approach to configure it Globally for the DbContext?

from audit.net.

thepirat000 avatar thepirat000 commented on July 22, 2024

You could do that, but you should call _auditContext.ReloadDatabaseValues = true AFTER calling _helper.SetConfig(), otherwise it will be overridden by the default configuration:

public MyContext(string connectionString) : base(GetOptions(connectionString))
{
    _auditContext = new DefaultAuditContext(this);
    _helper.SetConfig(_auditContext);
    _auditContext.ReloadDatabaseValues = true;
}

But the recommended way to set the configuration globally is by using the fluent API, for example:

Audit.EntityFramework.Configuration.Setup()
    .ForAnyContext(cfg => cfg.ReloadDatabaseValues());

Or:

Audit.EntityFramework.Configuration.Setup()
    .ForContext<MyContext>(cfg => cfg.ReloadDatabaseValues());

Also, I've just realized I didn't add a property to the AuditDbContextAttribute, but will do it for the next release.

So, you will be able to set the configuration like this:

[AuditDbContext(ReloadDatabaseValues = true, Mode = AuditOptionMode.OptIn, IncludeEntityObjects = false, AuditEventType = "EF")]
public class MyContext : DbContext
{
   ...
}

from audit.net.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.