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)
-
0
Hi,
Because mongodb has no entity tracking, so mongodb does not support entity change auditing.
Currently you can manually record entity changes.
-
2
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; } }