AuditorBase Issues.

Posts   
 
    
Posts: 5
Joined: 16-Aug-2007
# Posted on: 17-Aug-2007 23:13:51   

We are overriding the AuditUpdateOfExistingEntity method in our AuditorBase derrived class.

We have three issues.

(1) Inside AuditUpdateOfExistingEntity, we would like to determine what fields have been changed. What is a good way to do this???

We hacked something like this, but it seems like there should be an easier way.


public override void AuditUpdateOfExistingEntity(IEntityCore entity) {
    CommonEntityBase commonEntityBase = entity as CommonEntityBase;
    foreach (EntityField2 field in commonEntityBase.Fields) {
        if (isDifferent(field)) {
            // ...WriteAudit records.
        }
    }
}

private bool isDifferent(EntityField2 entityField) {
    switch (entityField.DataType.ToString()) {
        case "System.String": {
            return string.Compare((string) entityField.DbValue, (string) entityField.CurrentValue, false) != 0;
        }
        case "System.Int32": {
            return Convert.ToInt32(entityField.DbValue) != Convert.ToInt32(entityField.CurrentValue);
        }
        case "System.Decimal": {
            return Decimal.Compare((Decimal) entityField.DbValue, (Decimal) entityField.CurrentValue) != 0;
        }
        case "System.Boolean": {
            return (Boolean) entityField.DbValue != (Boolean) entityField.CurrentValue;
        }
        default: {
            return false;
        }
    }
}

(2) Inside both AuditInsertOfNewEntity and AuditUpdateOfExistingEntity

entity.IsNew is always false. commonEntityBase.Fields.IsChangedInThisEditCycle is always false. commonEntityBase.Fields.State = OutOfSync

In AuditDeleteOfEntity: commonEntityBase.MarkedForDeletion = false;

Is this expected?

(3) When we cast the entity paramater to an actual Entity type, in the Delete and Update overrides all of the properties throw an ORMEntityOutOfSyncException exception, however in the delete override the properties have their expected values in them.

Is this also expected??

To furthur complicate things, we are using the LLBLGenProDataSource2 bound to a FormView and a GridView. LivePersistence is set to True. There is no code in the code behind.

Thanks! Tony Stratton

P.S. The schema of the audit is intended to have one new data row for each changed Property. (for example, here is the Audit records that we desire, which is why we need to know what property changed...)

(TableName, ColumnName, AuditType, OldColumnValue, NewColumnValue, UserId, DateTime, etc, etc)

Posts: 5
Joined: 16-Aug-2007
# Posted on: 17-Aug-2007 23:29:26   

A little more info. If we don't use the LLBLGenProDataSource2, and save our entity with "refetchAfterSave" set to true, then DBValue == CurrentValue, and we can no longer determine which columns have changed with our previous solution.

Does this new auditing feature even help us track column level changes?

I saw the AuditEntityFieldSet, but I don't know how helpful that will be because I don't want to track all field sets, but just sets that result in a database insert or update.

Thanks. Tony

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 18-Aug-2007 08:48:24   

Hi Tony,

(1) Inside AuditUpdateOfExistingEntity, we would like to determine what fields have been changed. What is a good way to do this???

P.S. The schema of the audit is intended to have one new data row for each changed Property. (for example, here is the Audit records that we desire, which is why we need to know what property changed...)

I would go on AuditEntityFieldSet:

/// <summary>
/// Audits when an entity field is set succesfully to a new value.
/// </summary>
/// <param name="entity">The entity a field was set to a new value.</param>
/// <param name="fieldIndex">Index of the field which got a new value.</param>
/// <param name="originalValue">The original value of the field with the index passed in 
/// before it received a new value.</param>
public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
{
    // used to store the change experimented by a field.
    string originalValueAsString = string.Empty;
    string currentValueAsString = string.Empty;
    
    // sets VALUE OR NULL for originalValue and uses it as string.
    if (originalValue != null)
    {
        originalValueAsString = originalValue.ToString();
    }
    else
    {
        originalValueAsString = "NULL";
    }

    // sets VALUE OR NULL for currentValue and uses it as string.
    if (entity.GetCurrentFieldValue(fieldIndex) != null)
    {
        currentValueAsString = entity.GetCurrentFieldValue(fieldIndex).ToString();
    }
    else
    {
        currentValueAsString = "NULL";
    }

    string actionData = string.Format("WhatEntity:{0}. FieldSet: {1}. OriginalValue: {2}. CurrentValue: {3}",
            GetPrimaryKeyInfoFromEntity(entity),
            ((IEntity)entity).Fields[fieldIndex].Name,
            originalValueAsString, currentValueAsString);

    // create a new audit unit
    AuditInfoEntity auditInfo = new AuditInfoEntity();
    auditInfo.AffectedEntityName = entity.LLBLGenProEntityName;
    auditInfo.ActionDateTime = DateTime.Now;
    auditInfo.AuditActionTypeId = AuditType.EntityFieldSet;
    auditInfo.UserId = GetCurrentUserID();
    auditInfo.ActionData = actionData;

    // adds for further DB persist
    _auditInfoEntities.Add(auditInfo);
}

/// <summary>
/// Gets the primary key info from entity.
/// </summary>
/// <param name="entity">A valid instance of IEntity object.</param>
/// <returns>Formatted string contained PK fields of the given entity</returns>
private string GetPrimaryKeyInfoFromEntity(IEntityCore entity)
{
    // gets primary key fields
    List<IEntityField> pkFields = ((IEntity)entity).PrimaryKeyFields;

    // collect PK fields if the entity isn't new
    string strPKEntityInfo = string.Empty;
    if (!entity.IsNew)
    {
        // construct formatted string with pk fields
        strPKEntityInfo = "PK(";
        foreach (IEntityField pkField in pkFields)
        {
            strPKEntityInfo += string.Format("{0}:{1}, ", pkField.Name, pkField.CurrentValue.ToString());
        }

        // delete the extra ", " and ends with ")"
        strPKEntityInfo = strPKEntityInfo.Remove(strPKEntityInfo.Length - 2, 2);
        strPKEntityInfo += ")";
    }

    // returns PK info
    return strPKEntityInfo;
}

So, when entities would be saved, your audit entities would be saved as well.

(2) Inside both AuditInsertOfNewEntity and AuditUpdateOfExistingEntity

entity.IsNew is always false. commonEntityBase.Fields.IsChangedInThisEditCycle is always false. commonEntityBase.Fields.State = OutOfSync

In AuditDeleteOfEntity: commonEntityBase.MarkedForDeletion = false;

Is this expected?

When the program go into _AuditInsertOfNewEntity _and _AuditUpdateOfExistingEntity _the entities were saved successfully into the database, so that properties (State, IsNew, etc) were updated. Do you need access such fields for some special reason?

David Elizondo | LLBLGen Support Team
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39910
Joined: 17-Aug-2003
# Posted on: 18-Aug-2007 10:22:47   

Additionally: to check if a field is changed, just check: myEntity.Fields[index].IsChanged

if it's true, the field has been changed. This flag is set by llblgen pro so it can quickly check which fields are actually changed. This uses logic based on the IsNew flag, the DBValue, and if the field was already null or not etc., so the logic you'd normally use to see if it was really changed, has already been applied to the field for you. simple_smile

Frans Bouma | Lead developer LLBLGen Pro
Posts: 5
Joined: 16-Aug-2007
# Posted on: 20-Aug-2007 20:53:33   

Otis,

Inside the Audit method (AuditInsertOfNewEntity, AuditUpdateOfExistingEntity, etc)

myEntity.Fields[index].IsChanged will always be false.

So I can't use it as you suggested. I think I have to track the column changes with the (AuditEntityFieldSet) method, which is a bit of a pain, but I think close enough for what I need.

I'll post my solution when I get it working later today.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39910
Joined: 17-Aug-2003
# Posted on: 20-Aug-2007 20:57:27   

EmpireTStratton wrote:

Otis,

Inside the Audit method (AuditInsertOfNewEntity, AuditUpdateOfExistingEntity, etc)

myEntity.Fields[index].IsChanged will always be false.

So I can't use it as you suggested. I think I have to track the column changes with the (AuditEntityFieldSet) method, which is a bit of a pain, but I think close enough for what I need.

It's actually what it's for simple_smile . Auditing after the save deals with saved entities, that method is simply meant to log that there's been a save.

The auditing is designed in such a way that it can record information along the way, during the life of the entity. At the end, it gets a call to return the entities to save inside the transaction, if applicable. You then wrap everything up and return that info.

I'll post my solution when I get it working later today.

Cool simple_smile

Frans Bouma | Lead developer LLBLGen Pro
Posts: 5
Joined: 16-Aug-2007
# Posted on: 24-Aug-2007 00:52:27   

Here is a method to log changed columns. Sorry about all of the regions, code standards and all...


[DependencyInjectionInfo(typeof(IEntity2), "AuditorToUse")]
[Serializable()]
public class GeneralAuditor : AuditorBase {

  #region Helper Class

  private class ChangedFieldStorage {

    #region Data Members

    private object _originalValue;
    private EntityField2 _field;

    #endregion

    #region Properties

    public object OriginalValue {
      get { return _originalValue; }
    }

    public EntityField2 Field {
      get { return _field; }
      set { _field = value; }
    }

    #endregion

    #region Constructor

    public ChangedFieldStorage(object originalValue, EntityField2 field) {
      _originalValue = originalValue;
      _field = field;
    }

    #endregion
  }

  #endregion

  #region Enums

  private enum AuditType {
    InsertOfNewEntity = 1,
    UpdateOfExistingEntity,
    DeleteOfEntity,
    DereferenceOfRelatedEntity,
    ReferenceOfRelatedEntity,
    //DirectDeleteOfEntities,
    //DirectUpdateOfEntities,
    //EntityFieldSet,
  }

  #endregion

  #region Data Members

  private ChangedFieldStorage[] _changedFields; 
  private List<AuditInfoEntity> _auditInfoEntities;

  #endregion

  #region Constructor

  public GeneralAuditor() {
    _auditInfoEntities = new List<AuditInfoEntity>();
  }

  #endregion

  #region Override Methods

  public override void AuditDereferenceOfRelatedEntity(IEntityCore entity, IEntityCore relatedEntity, string mappedFieldName) {
    #region Dereference

    AuditInfoEntity auditInfo = getCurrentEntityInformation(entity);
    auditInfo.AuditType = (int)AuditType.DereferenceOfRelatedEntity;
    
    _auditInfoEntities.Add(auditInfo);

    #endregion
  }

  public override void AuditReferenceOfRelatedEntity(IEntityCore entity, IEntityCore relatedEntity, string mappedFieldName) {
    #region Reference

    AuditInfoEntity auditInfo = getCurrentEntityInformation(entity);
    auditInfo.AuditType = (int)AuditType.ReferenceOfRelatedEntity;

    _auditInfoEntities.Add(auditInfo);

    #endregion
  }

  public override void AuditDeleteOfEntity(IEntityCore entity) {
    #region Delete

    AuditInfoEntity auditInfo = getCurrentEntityInformation(entity);
    auditInfo.AuditType = (int)AuditType.DeleteOfEntity;
    
    _auditInfoEntities.Add(auditInfo);
    
    #endregion
  }

  public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue) {
    #region Field Set, for column level auditing

    CommonEntityBase commonEntityBase = entity as CommonEntityBase;

    if (commonEntityBase == null) {
      throw new ArgumentException("Bad Entity Type : Expecting a CommonEntityBase", "entity");
    }

    if (_changedFields == null) {
      _changedFields = new ChangedFieldStorage[commonEntityBase.Fields.Count];
    }

    //
    // Only update the Field property, and not the original value.
    //
    if (_changedFields[fieldIndex] == null) {
      _changedFields[fieldIndex] = new ChangedFieldStorage(originalValue, (EntityField2)commonEntityBase.Fields[fieldIndex]);
    } else {
      _changedFields[fieldIndex].Field = (EntityField2) commonEntityBase.Fields[fieldIndex];
    }

    #endregion
  }

  public override void AuditInsertOfNewEntity(IEntityCore entity) {
    #region Insert

    addColumnAuditEntries(entity, AuditType.InsertOfNewEntity);

    #endregion
  }

  public override void AuditUpdateOfExistingEntity(IEntityCore entity) {
    #region Update

    addColumnAuditEntries(entity, AuditType.UpdateOfExistingEntity);

    #endregion
  }

  private void addColumnAuditEntries(IEntityCore entity, AuditType auditType) {
    #region Turn each column edit into a row in the audit table.

    if (_changedFields == null) {
      return;
    }

    for (int i = 0 ; i < _changedFields.Length ; i++) {
      ChangedFieldStorage changedField = _changedFields[i];

      if (changedField != null) {
        AuditInfoEntity auditInfo = getCurrentEntityInformation(entity);
        auditInfo.AuditType = (int)auditType;

        auditInfo.ColumnName = changedField.Field.Name;
        auditInfo.OldValue = stringizeValue(changedField.OriginalValue, AuditInfoFields.OldValue.MaxLength);
        auditInfo.NewValue = stringizeValue(changedField.Field.CurrentValue, AuditInfoFields.NewValue.MaxLength);
        _auditInfoEntities.Add(auditInfo);
      }
    }

    #endregion
  }

  public override IList GetAuditEntitiesToSave() {
    #region Get Audit Entities to Save

    return _auditInfoEntities;

    #endregion
  }

  public override void TransactionCommitted() {
    #region Reset data members to nothing to prepare for the next auditable transaction.

    _changedFields = null;
    _auditInfoEntities.Clear();

    #endregion
  }

  #endregion

  #region Helper Methods

  private string stringizeValue(object value, int maxLength) {
    #region Stringize Object Value

    string returnValue = string.Empty;

    if (maxLength < 10) {
      throw new ArgumentException("maxLength should be >=10", "maxLength");
    }

    if (value == null) {
      returnValue = "Null Value";
    } else {
      returnValue = value.ToString();
    }

    return returnValue.Substring(0, Math.Min(returnValue.Length, maxLength));

    #endregion
  }

  private string getTableKey(List<IEntityField2> primaryKeyFields, int maxLength) {
    #region Get Table Key

    if (primaryKeyFields.Count == 0) {
      return "No Keys";
    }

    string returnValueAsString;

    if (primaryKeyFields.Count == 1) {
      object returnValue = primaryKeyFields[0].DbValue ?? primaryKeyFields[0].CurrentValue ?? "Null";
      returnValueAsString = returnValue.ToString();
    } else {
      returnValueAsString = string.Empty;

      foreach (IEntityField2 field in primaryKeyFields) {
        object returnValue = field.DbValue ?? "Null";
        returnValueAsString += string.Format("{0}|", returnValue.ToString());
      }
    }

    return returnValueAsString.Substring(0, Math.Min(returnValueAsString.Length, maxLength));

    #endregion
  }

  private AuditInfoEntity getCurrentEntityInformation(IEntityCore entity) {
    #region Get Current Entity Information

    CommonEntityBase commonEntityBase = entity as CommonEntityBase;
    if (commonEntityBase == null) {
      throw new ArgumentException("Bad Entity Type : Expecting a CommonEntityBase", "entity");
    }

    AuditInfoEntity auditInfo = new AuditInfoEntity();
    auditInfo.AuditorToUse = null;
    auditInfo.TableId = entity.LLBLGenProEntityTypeValue;
    auditInfo.TableName = entity.LLBLGenProEntityName.Substring(0, Math.Min(entity.LLBLGenProEntityName.Length, AuditInfoFields.TableName.MaxLength));
    auditInfo.TableKey = getTableKey(commonEntityBase.PrimaryKeyFields, AuditInfoFields.TableKey.MaxLength);
    
    try {
      auditInfo.UserId = "UserId";          }
    catch {
      auditInfo.UserId = "unknown";
    }

    return auditInfo;

    #endregion
  }

  #endregion

}

Happy hacking!! -- E_TS

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 24-Aug-2007 09:31:01   

Hi E_TS,

Your Auditor looks nice. First I'm wondering that you would receive an **Overflow **for inject into _IEntity2 _without validate whether the entity is or not an _AuditInfoEntity _at your audit methods. But looking closely I found your auditInfo.AuditorToUse = null; line. This makes the trick. I did that in the _InitClassMembers _entity method or by checking in every audit method however what you did is also a good valid option wink

I saw your private methods. Those look good. You can make your own _AuditHelperClasses _assembly for future auditors you do, so in the future wouldn't be that hard.

I don't see an auditInfo.dateChanged datetime field or something like that at your code. Isn't it a mandatory information in your case?

Good to know you make it. Regards.

David Elizondo | LLBLGen Support Team