Open Closed

How to enable basic entity auditing for MongoDB #683


User avatar
0
michael.sudnik created
  • ABP Framework version: v4.0.0
  • UI type: MVC
  • Tiered (MVC) or Identity Server Seperated (Angular): yes
  • Exception message and stack trace:
  • Steps to reproduce the issue:

Hi,

I have been trying to get the entity auditing working, but then I found out through the documentation that it is not supported for MongoDB. https://docs.abp.io/en/abp/latest/Audit-Logging

Is there a reason that this has not yet been implemented for MongoDB? Can you let me know if this is planned to be supported? Can you let me know how I might go about implementing this myself?

Many thanks,

Mike


2 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Because mongodb has no entity tracking, so mongodb does not support entity change auditing.

    Currently you can manually record entity changes.

  • User Avatar
    2
    michael.sudnik created

    If anyone else is interested in doing this, here is my own implementation to add some simple entity change tracking by overriding the default MongoDbRepository class.

    public class MongoDbEntityChangeRepository<TMongoDbContext, TEntity, TKey>
            : MongoDbRepository<TMongoDbContext, TEntity, TKey>
                , IAuditedRepository<TEntity, TKey>
            where TMongoDbContext : IAbpMongoDbContext
            where TEntity : class, IEntity<TKey>
        {
            public MongoDbEntityChangeRepository(IMongoDbContextProvider<TMongoDbContext> dbContextProvider)
                : base(dbContextProvider)
            {
            }
    
            private readonly object _serviceProviderLock = new object();
    
            private TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference)
            {
                if (reference == null)
                {
                    lock (_serviceProviderLock)
                    {
                        if (reference == null)
                        {
                            reference = (TRef)ServiceProvider.GetRequiredService(serviceType);
                        }
                    }
                }
    
                return reference;
            }
    
            public EntityChangeLogger EntityChangeLogger => LazyGetRequiredService(typeof(EntityChangeLogger), ref _entityChangeLogger);
            private EntityChangeLogger _entityChangeLogger;
    
    
            /// <summary>
            /// Use this method to prevent fetching the entity again, if you already have it
            /// </summary>
            /// <param name="original"></param>
            /// <param name="updateEntity"></param>
            /// <param name="autoSave"></param>
            /// <param name="cancellationToken"></param>
            /// <returns></returns>
            public async Task<TEntity> UpdateAsync(
                TEntity original,
                Action<TEntity> updateEntity,
                bool autoSave = false,
                CancellationToken cancellationToken = default(CancellationToken))
            {
                var previous = EntityChangeLogger.CloneEntity<TEntity, TKey>(original);
    
                updateEntity(original);
                var updated = original;
    
                updated = await base.UpdateAsync(updated, autoSave, cancellationToken);
    
                EntityChangeLogger.LogEntityUpdated<TEntity, TKey>(previous, updated);
    
                return updated;
            }
    
            public override async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = new CancellationToken())
            {
                TEntity previous = null;
    
                bool entityHistoryEnabled = EntityChangeLogger.IsEntityHistoryEnabled<TEntity, TKey>();
    
                if (entityHistoryEnabled)
                {
                    previous = await GetAsync(entity.Id, cancellationToken: cancellationToken);
                }
    
                var result = await base.UpdateAsync(entity, autoSave, cancellationToken);
    
                if (entityHistoryEnabled)
                {
                    EntityChangeLogger.LogEntityUpdated<TEntity, TKey>(previous, entity);
                }
    
                return result;
            }
    
            public override async Task DeleteAsync(
                TEntity entity,
                bool autoSave = false,
                CancellationToken cancellationToken = default)
            {
                await base.DeleteAsync(entity, autoSave, cancellationToken);
    
                EntityChangeLogger.LogEntityDeleted<TEntity, TKey>(entity);
            }
    
            public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = new CancellationToken())
            {
                var result = await base.InsertAsync(entity, autoSave, cancellationToken);
    
                EntityChangeLogger.LogEntityCreated<TEntity, TKey>(result);
    
                return result;
            }
        }
    
    public class EntityChangeLogger
        {
            private readonly IAuditingManager _auditingManager;
            private readonly IClock _clock;
            private readonly IAuditingHelper _auditingHelper;
    
            public EntityChangeLogger(IAuditingManager auditingManager, IClock clock, IAuditingHelper auditingHelper)
            {
                _auditingManager = auditingManager;
                _clock = clock;
                _auditingHelper = auditingHelper;
            }
    
            public void LogEntityCreated<T, TKey>(T current) where T : class, IEntity<TKey>
            {
                var entityType = typeof(T);
    
                if (!_auditingHelper.IsEntityHistoryEnabled(entityType))
                    return;
    
                _auditingManager.Current.Log.EntityChanges.Add(new EntityChangeInfo()
                {
                    ChangeTime = _clock.Now,
                    ChangeType = EntityChangeType.Created,
                    EntityId = GetEntityId(current),
                    EntityTenantId = null,
                    EntityTypeFullName = entityType.FullName,
                    PropertyChanges = GetPropertyChanges<T, TKey>(null, current, true)
                });
            }
    
            public void LogEntityDeleted<T, TKey>(T previous) where T : class, IEntity<TKey>
            {
                var entityType = typeof(T);
    
                if (!_auditingHelper.IsEntityHistoryEnabled(entityType))
                    return;
    
                _auditingManager.Current.Log.EntityChanges.Add(new EntityChangeInfo()
                {
                    ChangeTime = _clock.Now,
                    ChangeType = EntityChangeType.Deleted,
                    EntityId = GetEntityId(previous),
                    EntityTenantId = null,
                    EntityTypeFullName = entityType.FullName,
                    PropertyChanges = GetPropertyChanges<T, TKey>(previous, null, true)
                });
            }
    
            public bool IsEntityHistoryEnabled<T, TKey>() where T : class, IEntity<TKey>
            {
                var entityType = typeof(T);
    
                return _auditingHelper.IsEntityHistoryEnabled(entityType);
            }
    
            public void LogEntityUpdated<T, TKey>(T previous, T current) where T : class, IEntity<TKey>
            {
                var entityType = typeof(T);
    
                if (!IsEntityHistoryEnabled<T, TKey>())
                    return;
    
                var notNullEntity = GetNotNull(previous, current);
    
                _auditingManager.Current.Log.EntityChanges.Add(new EntityChangeInfo()
                {
                    ChangeTime = _clock.Now,
                    ChangeType = EntityChangeType.Updated,
                    EntityId = GetEntityId(notNullEntity),
                    EntityTenantId = null,
                    EntityTypeFullName = entityType.FullName,
                    PropertyChanges = GetPropertyChanges<T, TKey>(previous, current, false)
                });
            }
    
            private static ConcurrentDictionary<Type, PropertyInfos> _propertyInfoCache = new ConcurrentDictionary<Type, PropertyInfos>();
    
            private class PropertyInfos
            {
                public PropertyInfos(PropertyInfo id, PropertyInfo extraProperties)
                {
                    Id = id;
                    ExtraProperties = extraProperties;
                }
    
                public PropertyInfo Id { get; private set; }
                public PropertyInfo ExtraProperties { get; private set; }
            }
    
            public T CloneEntity<T, TKey>(T entity) where T : class, IEntity<TKey>
            {
                // create a clone, without going back to the database again
                var doc = JsonSerializer.Serialize(entity);
    
                var clone = JsonSerializer.Deserialize<T>(doc);
    
                // set protected properties
                // - Id
                // - Extra Properties
    
                var entityType = typeof(T);
    
                var propertyInfo = _propertyInfoCache.GetOrAdd(entityType, CreatePropertyInfos<TKey>);
    
                propertyInfo.Id.SetValue(clone, entity.Id);
    
                if (entityType.IsAssignableTo<IHasExtraProperties>())
                {
                    IHasExtraProperties e = (IHasExtraProperties)entity;
    
                    propertyInfo.ExtraProperties.SetValue(clone, SimpleClone(e.ExtraProperties));
                }
    
                return clone;
            }
    
            private PropertyInfos CreatePropertyInfos<TKey>(Type entityType)
            {
                return new PropertyInfos(
                    entityType.GetProperty(nameof(IEntity<TKey>.Id)),
                        entityType.GetProperty(nameof(IHasExtraProperties.ExtraProperties)));
            }
    
            private T SimpleClone<T>(T toClone)
            {
                var doc = JsonSerializer.Serialize(toClone);
    
                return JsonSerializer.Deserialize<T>(doc);
            }
    
            private List<EntityPropertyChangeInfo> GetPropertyChanges<T, TKey>(T previous, T current, bool logIfNoChange) where T : class, IEntity<TKey>
            {
                List<EntityPropertyChangeInfo> result = new List<EntityPropertyChangeInfo>();
    
                foreach (var propertyInfo in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
                {
                    if (ShouldSavePropertyHistory(previous, current, propertyInfo, logIfNoChange, out string previousValue, out string currentValue))
                    {
                        result.Add(new EntityPropertyChangeInfo
                        {
                            NewValue = currentValue,
                            OriginalValue = previousValue,
                            PropertyName = propertyInfo.Name,
                            PropertyTypeFullName = propertyInfo.PropertyType.FullName
                        });
                    }
                }
    
                return result;
            }
    
            private bool ShouldSavePropertyHistory<T>(T previous, T current, PropertyInfo propertyInfo, bool logIfNoChange, out string previousValue, out string currentValue)
            {
                previousValue = null;
                currentValue = null;
    
                if (propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
                {
                    return false;
                }
    
                var entityType = typeof(T);
    
                if (entityType.IsDefined(typeof(DisableAuditingAttribute), true))
                {
                    if (!propertyInfo.IsDefined(typeof(AuditedAttribute), true))
                    {
                        return false;
                    }
                }
    
                if (IsBaseAuditProperty(propertyInfo, entityType))
                {
                    return false;
                }
    
                previousValue = previous != null ? JsonSerializer.Serialize(propertyInfo.GetValue(previous)).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength) : null;
                currentValue = current != null ? JsonSerializer.Serialize(propertyInfo.GetValue(current)).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength) : null;
    
                var isModified = !(previousValue?.Equals(currentValue) ?? currentValue == null);
                if (isModified)
                {
                    return true;
                }
    
                return logIfNoChange;
            }
    
            private bool IsBaseAuditProperty(PropertyInfo propertyInfo, Type entityType)
            {
                if (entityType.IsAssignableTo<IHasCreationTime>()
                    && propertyInfo.Name == nameof(IHasCreationTime.CreationTime))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<IMayHaveCreator>()
                    && propertyInfo.Name == nameof(IMayHaveCreator.CreatorId))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<IMustHaveCreator>()
                    && propertyInfo.Name == nameof(IMustHaveCreator.CreatorId))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<IHasModificationTime>()
                    && propertyInfo.Name == nameof(IHasModificationTime.LastModificationTime))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<IModificationAuditedObject>()
                    && propertyInfo.Name == nameof(IModificationAuditedObject.LastModifierId))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<ISoftDelete>()
                    && propertyInfo.Name == nameof(ISoftDelete.IsDeleted))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<IHasDeletionTime>()
                    && propertyInfo.Name == nameof(IHasDeletionTime.DeletionTime))
                {
                    return true;
                }
    
                if (entityType.IsAssignableTo<IDeletionAuditedObject>()
                    && propertyInfo.Name == nameof(IDeletionAuditedObject.DeleterId))
                {
                    return true;
                }
    
                return false;
            }
    
            private object GetNotNull(object obj1, object obj2)
            {
                if (obj1 != null)
                    return obj1;
                if (obj2 != null)
                    return obj2;
    
                throw new NotSupportedException();
            }
    
            private string GetEntityId(object entityAsObj)
            {
                if (!(entityAsObj is IEntity entity))
                {
                    throw new AbpException($"Entities should implement the {typeof(IEntity).AssemblyQualifiedName} interface! Given entity does not implement it: {entityAsObj.GetType().AssemblyQualifiedName}");
                }
    
                var keys = entity.GetKeys();
                if (keys.All(k => k == null))
                {
                    return null;
                }
    
                return keys.JoinAsString(",");
            }
    
            protected virtual Guid? GetTenantId(object entity)
            {
                if (!(entity is IMultiTenant multiTenantEntity))
                {
                    return null;
                }
    
                return multiTenantEntity.TenantId;
            }
        }
    
Made with ❤️ on ABP v9.0.0-preview Updated on September 19, 2024, 10:13