- Home
- LLBLGen Pro
- LLBLGen Pro Runtime Framework
AuditorBase Issues.
Joined: 16-Aug-2007
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)
Joined: 16-Aug-2007
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
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?
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.
Joined: 16-Aug-2007
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.
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 . 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
Joined: 16-Aug-2007
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
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
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.