Reading entities with PrefetchPath triggers AuditReferenceOfRelatedEntity

Posts   
 
    
Posts: 10
Joined: 18-Feb-2024
# Posted on: 25-Feb-2024 16:02:18   

Hi everyone,

The following piece of code triggers the AuditReferenceOfRelatedEntity on DepartmentGradeClassesPerWeekEntity

var classesCountPerWeekWithSchoolDepartment = await meta.DepartmentGradeClassesPerWeek.Where(x => 
x.SchoolDepartment.SchoolId == request.SchoolId && x.IsActive)
.WithPath(x => x.Prefetch(y => y.SchoolDepartment)).ToListAsync();

Is this the intended behavior? How to avoid triggering AuditReferenceOfRelatedEntity while performing read actions like the one mentioned above?

Thanks in advance

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39617
Joined: 17-Aug-2003
# Posted on: 26-Feb-2024 09:51:56   

The trees are merged in memory and there's no other way to intercept the reference assignment for auditing purposes (as after teh fetch the references are there but the auditor won't trigger), hence the audit action is triggered.

Frans Bouma | Lead developer LLBLGen Pro
Posts: 10
Joined: 18-Feb-2024
# Posted on: 26-Feb-2024 10:34:16   

I don't necessarily need to avoid this behavior but is there any way to determine what triggered the auditing logic at the Auditor class?

What I'm trying to achieve is to differentiate between reads and writes. Is it possible for me to pass something to the Auditor that I can use to check for that? Or if there's any other way please advice.

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 27-Feb-2024 11:00:26   

What exactly do you need to check/audit when referencing a related entity?

Btw, the OnAuditReferenceOfRelatedEntity receives the related entity as a parameter, you can check on the IsNew property, to see to determine if it's a new entity or not.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39617
Joined: 17-Aug-2003
# Posted on: 27-Feb-2024 11:13:06   

the prefetch paths are built on top of the existing mechanisms of building entity graphs so there's no way to switch it off. Additionally to what Walaa said, you could try using a [ThreadStatic] marked static variable (keep in mind that initializing it will run only once!) and set that to a value before the fetch and read it in the auditor and then act upon it. It's not ideal tho, forgetting to set the value and you'll get a false positive.

Frans Bouma | Lead developer LLBLGen Pro
Posts: 10
Joined: 18-Feb-2024
# Posted on: 29-Feb-2024 14:42:26   

When exactly should I set the variables marked with [ThreadStatic]? Sometimes it's set to the default values, below is the scenario

I perform a save action by some user and then I perform a direct delete action by another user

below is how both auditors look like: SchoolGradeAuditor

[DependencyInjectionInfo(typeof(SchoolGradeEntity), "AuditorToUse")]
[Serializable]
public class SchoolGradeAuditor() : AuditorBase
{
    private List<ActivityLogEntity> _auditInfoEntities = [];
    private SchoolGradeEntityAuditingHelper _schoolGradeEntityAuditingHelper = new();
    [ThreadStatic] public static int UserId = 0;
    [ThreadStatic] public static int? SchoolId = null;
    [ThreadStatic] public static string UserName = "";
    [ThreadStatic] public static bool IsPrefetched = true;
    [ThreadStatic] public static int? IdOfEntityRelatedToDeletedEntities = null;
    [ThreadStatic] public static string NameEnglishOfEntityRelatedToDeletedEntities = "";

    public static void SetUserInfo(IHttpContextAccessor httpContextAccessor)
    {
        var authResult = httpContextAccessor.HttpContext.User.GetUserId();
        if (authResult.IsTrue && authResult.Value > 0)
            UserId = authResult.Value;

        var userNameResult = httpContextAccessor.HttpContext.User.GetUserName();
        if (userNameResult.IsTrue)
            UserName = userNameResult;

        var schoolResult = httpContextAccessor.HttpContext.User.GetSchoolId();
        if (schoolResult.IsTrue && schoolResult.Value > 0)
            SchoolId = schoolResult;
    }

    public override void AuditDeleteOfEntity(IEntityCore entity)
    {
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = entity.LLBLGenProEntityName,
            FieldName = null,
            ActionTypeIndex = (int)Common.AuditType.DeleteOfEntity,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }

    public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
    {
        var messages = _schoolGradeEntityAuditingHelper.EntityFieldMessageSelector(fieldIndex, originalValue?.ToString()?? "", entity);
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = entity.LLBLGenProEntityName,
            FieldName = entity.Fields.GetFieldNames()[fieldIndex],
            MsgEnglish = messages.MessageEnglish,
            ActionTypeIndex = (int)Common.AuditType.EntityFieldSet,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }

    public override void AuditDereferenceOfRelatedEntity(IEntityCore entity, IEntityCore relatedEntity,
        string mappedFieldName)
    {
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = entity.LLBLGenProEntityName,
            FieldName = null,
            ActionTypeIndex = (int)Common.AuditType.DereferenceOfRelatedEntity,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }
    
    public override void AuditInsertOfNewEntity(IEntityCore entity)
    {
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = entity.LLBLGenProEntityName,
            FieldName = null,
            ActionTypeIndex = (int)Common.AuditType.InsertOfNewEntity,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }

    public override void AuditReferenceOfRelatedEntity(IEntityCore entity, IEntityCore relatedEntity,
        string mappedFieldName)
    {
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = entity.LLBLGenProEntityName,
            FieldName = null,
            ActionTypeIndex = (int)Common.AuditType.ReferenceOfRelatedEntity,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }
    
    public override IList GetAuditEntitiesToSave()
    {
        return _auditInfoEntities;
    }

    public override void TransactionCommitted()
    {
        _auditInfoEntities.Clear();
    }
}

DepartmentGradeClassesPerWeekAuditor

[DependencyInjectionInfo(typeof(DepartmentGradeClassesPerWeekEntity), "AuditorToUse")]
[Serializable]
public class DepartmentGradeClassesPerWeekAuditor() : AuditorBase
{
    private List<ActivityLogEntity> _auditInfoEntities = [];
    private DepartmentGradeClassesPerWeekEntityAuditingHelper _departmentGradeClassesPerWeekEntityAuditingHelper = new();
    [ThreadStatic] public static int UserId = 0;
    [ThreadStatic] public static int? SchoolId = null;
    [ThreadStatic] public static string UserName = "";
    [ThreadStatic] public static bool IsPrefetched = true;
    [ThreadStatic] public static int? IdOfEntityRelatedToDeletedEntities = null;
    [ThreadStatic] public static string NameEnglishOfEntityRelatedToDeletedEntities = "";

    public static void SetUserInfo(IHttpContextAccessor httpContextAccessor)
    {
        var authResult = httpContextAccessor.HttpContext.User.GetUserId();
        if (authResult.IsTrue && authResult.Value > 0)
            UserId = authResult.Value;

        var userNameResult = httpContextAccessor.HttpContext.User.GetUserName();
        if (userNameResult.IsTrue)
            UserName = userNameResult;

        var schoolResult = httpContextAccessor.HttpContext.User.GetSchoolId();
        if (schoolResult.IsTrue && schoolResult.Value > 0)
            SchoolId = schoolResult;
    }

    public override void AuditDereferenceOfRelatedEntity(IEntityCore entity, IEntityCore relatedEntity,
        string mappedFieldName)
    {
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = entity.LLBLGenProEntityName,
            FieldName = null,
            ActionTypeIndex = (int)Common.AuditType.DereferenceOfRelatedEntity,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }

    public override void AuditReferenceOfRelatedEntity(IEntityCore entity, IEntityCore relatedEntity,
        string mappedFieldName)
    {
        //since prefetching related entity will trigger this method, we need to check if the entity is prefetched or not
        if (!IsPrefetched)
        {
            var messages = _departmentGradeClassesPerWeekEntityAuditingHelper.EntityReferenceMessageSelector(entity, relatedEntity, mappedFieldName);
            var logEntity = new ActivityLogEntity
            {
                UserId = UserId,
                SchoolId = SchoolId,
                EntityName = entity.LLBLGenProEntityName,
                FieldName = mappedFieldName,
                MsgEnglish = messages.MessageEnglish,
                ActionTypeIndex = (int)Common.AuditType.ReferenceOfRelatedEntity,
                DateCreatedUtc = DateTime.UtcNow,
            };

            _auditInfoEntities.Add(logEntity);
        }
    }

    public override void AuditDirectDeleteOfEntities(Type typeOfEntity, IPredicate filter, IRelationCollection relations, int numberOfEntitiesDeleted)
    {
        var messages = _departmentGradeClassesPerWeekEntityAuditingHelper.EntityDereferenceMessageSelector(
            typeOfEntity.Name, IdOfEntityRelatedToDeletedEntities.Value, NameEnglishOfEntityRelatedToDeletedEntities);
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = typeOfEntity.Name,
            MsgEnglish = messages.MessageEnglish,
            ActionTypeIndex = (int)Common.AuditType.DirectDeleteOfEntities,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }
    public override void AuditDeleteOfEntity(IEntityCore deletedEntity)
    {
        var messages = _departmentGradeClassesPerWeekEntityAuditingHelper.EntityDereferenceMessageSelector(
            deletedEntity.LLBLGenProEntityName, IdOfEntityRelatedToDeletedEntities.Value, NameEnglishOfEntityRelatedToDeletedEntities);
        var logEntity = new ActivityLogEntity
        {
            UserId = UserId,
            SchoolId = SchoolId,
            EntityName = deletedEntity.LLBLGenProEntityName,
            MsgEnglish = messages.MessageEnglish,
            ActionTypeIndex = (int)Common.AuditType.DirectDeleteOfEntities,
            DateCreatedUtc = DateTime.UtcNow,
        };

        _auditInfoEntities.Add(logEntity);
    }

    public override IList GetAuditEntitiesToSave()
    {
        return _auditInfoEntities;
    }

    public override void TransactionCommitted()
    {
        _auditInfoEntities.Clear();
    }
}

when they hit the auditor method, depends on the order I perform the actions for the first action the values are set as intended and for the second action they're default

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39617
Joined: 17-Aug-2003
# Posted on: 01-Mar-2024 09:00:34   

Reham wrote:

When exactly should I set the variables marked with [ThreadStatic]? Sometimes it's set to the default values, below is the scenario

I deliberately said: don't set these to a default. [ThreadStatic] initializations are run once so the second time the initialization isn't run. These flags should be meant to be used as thread local global variables for a single purpose, which is in this case: signaling an object (the auditor object) what context it is operating in, so it can make decisions.

The auditing system in our system is designed to audit operations on entities so the information related to these operations is available with the audit call. These audit calls come from a method in the entity. For instance the direct delete of entities call on an auditor comes from the method OnAuditDirectDeleteOfEntities in the entity that was deleted (or an instance of the type). It's a protected virtual method. You can override these (and other audit methods) in a partial class of CommonEntityBase which is the generated base class for all entities. Actions on an existing entity have the data of the entity the operation was performed on, but deleting an entity directly has not, as there are no entity instances, so you have to work with what's passed in.

You might want to log more than what the framework provides you. In that case you have to provide that information yourself. There are many ways to do that. One of them is the threadstatic route. Another is to set properties on the auditor object in an override of the audit methods like OnAuditDirectDeleteOfEntities.

Frans Bouma | Lead developer LLBLGen Pro