From f715ad65203049dc7b528ab0154879dfceea7f8f Mon Sep 17 00:00:00 2001 From: Halil ibrahim Kalkan Date: Tue, 10 Jul 2018 10:46:23 +0300 Subject: [PATCH] Implemented entity history --- .../Volo/Abp/Auditing/AuditLogActionInfo.cs | 5 +- .../Volo/Abp/Auditing/AuditLogInfo.cs | 21 +- .../Volo/Abp/Auditing/EntityChangeInfo.cs | 2 +- .../Abp/Auditing/EntityPropertyChangeInfo.cs | 27 +- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 57 ++- .../EntityHistory/EntityHistoryHelper.cs | 340 ++++++++++++++++++ .../EntityHistory/IEntityHistoryHelper.cs | 13 + .../EntityHistory/NullEntityHistoryHelper.cs | 26 ++ 8 files changed, 481 insertions(+), 10 deletions(-) create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogActionInfo.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogActionInfo.cs index abc8a9fcd0..4f103ca06b 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogActionInfo.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogActionInfo.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.Auditing { - public class AuditLogActionInfo + public class AuditLogActionInfo : IMultiTenant { + public Guid? TenantId { get; set; } + public string ServiceName { get; set; } public string MethodName { get; set; } diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogInfo.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogInfo.cs index 887d45b5bf..5de880a8cc 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogInfo.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditLogInfo.cs @@ -32,14 +32,14 @@ namespace Volo.Abp.Auditing public Dictionary ExtraProperties { get; } - public IList EntityChanges { get; } + public List EntityChanges { get; } public AuditLogInfo() { Actions = new List(); Exceptions = new List(); ExtraProperties = new Dictionary(); - EntityChanges = new List(); + EntityChanges = new List(); } public override string ToString() @@ -57,7 +57,7 @@ namespace Volo.Abp.Auditing foreach (var action in Actions) { sb.AppendLine($" - {action.ServiceName}.{action.MethodName} ({action.ExecutionDuration} ms.)"); - sb.AppendLine($" - {action.Parameters}"); + sb.AppendLine($" {action.Parameters}"); } } @@ -67,11 +67,22 @@ namespace Volo.Abp.Auditing foreach (var exception in Exceptions) { sb.AppendLine($" - {exception.Message}"); - sb.AppendLine($" - {exception}"); + sb.AppendLine($" {exception}"); } } - //TODO: EntityChanges + if (EntityChanges.Any()) + { + sb.AppendLine("- Entity Changes:"); + foreach (var entityChange in EntityChanges) + { + sb.AppendLine($" - [{entityChange.ChangeType}] {entityChange.EntityTypeFullName}, Id = {entityChange.EntityId}"); + foreach (var propertyChange in entityChange.PropertyChanges) + { + sb.AppendLine($" {propertyChange.PropertyName}: {propertyChange.OriginalValue} -> {propertyChange.NewValue}"); + } + } + } return sb.ToString(); } diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityChangeInfo.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityChangeInfo.cs index d931cfb789..a250b83f9e 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityChangeInfo.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityChangeInfo.cs @@ -17,7 +17,7 @@ namespace Volo.Abp.Auditing public Guid? TenantId { get; set; } - public ICollection PropertyChanges { get; set; } + public List PropertyChanges { get; set; } #region Not mapped diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityPropertyChangeInfo.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityPropertyChangeInfo.cs index 2a68fb3da6..53d45329c0 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityPropertyChangeInfo.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/EntityPropertyChangeInfo.cs @@ -1,7 +1,30 @@ -namespace Volo.Abp.Auditing +using System; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Auditing { - public class EntityPropertyChangeInfo + public class EntityPropertyChangeInfo : IMultiTenant { + /// + /// Maximum length of property. + /// Value: 96. + /// + public const int MaxPropertyNameLength = 96; + + /// + /// Maximum length of and properties. + /// Value: 512. + /// + public const int MaxValueLength = 512; + + /// + /// Maximum length of property. + /// Value: 512. + /// + public const int MaxPropertyTypeFullNameLength = 192; + + public Guid? TenantId { get; set; } + public virtual string NewValue { get; set; } public virtual string OriginalValue { get; set; } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index e5cbfcae66..1d4e355892 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -9,11 +9,14 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.EntityFrameworkCore.EntityHistory; using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; @@ -40,6 +43,12 @@ namespace Volo.Abp.EntityFrameworkCore public IAuditPropertySetter AuditPropertySetter { get; set; } + public IEntityHistoryHelper EntityHistoryHelper { get; set; } + + public IAuditingManager AuditingManager { get; set; } + + public ILogger> Logger { get; set; } + private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo = typeof(AbpDbContext) .GetMethod( @@ -52,6 +61,8 @@ namespace Volo.Abp.EntityFrameworkCore { GuidGenerator = SimpleGuidGenerator.Instance; EntityChangeEventHelper = NullEntityChangeEventHelper.Instance; + EntityHistoryHelper = NullEntityHistoryHelper.Instance; + Logger = NullLogger>.Instance; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -70,14 +81,35 @@ namespace Volo.Abp.EntityFrameworkCore public override int SaveChanges(bool acceptAllChangesOnSuccess) { - ChangeTracker.DetectChanges(); + //TODO: Reduce duplications with SaveChangesAsync + //TODO: Instead of adding entity changes to audit log, write them to uow and add to audit log only if uow succeed + ChangeTracker.DetectChanges(); + try { ChangeTracker.AutoDetectChangesEnabled = false; //TODO: Why this is needed? + + var auditLog = AuditingManager?.Current?.Log; + + List entityChangeList = null; + if (auditLog != null) + { + entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList()); + } + var changeReport = ApplyAbpConcepts(); + var result = base.SaveChanges(acceptAllChangesOnSuccess); + AsyncHelper.RunSync(() => EntityChangeEventHelper.TriggerEventsAsync(changeReport)); + + if (auditLog != null) + { + EntityHistoryHelper.UpdateChangeList(entityChangeList); + auditLog.EntityChanges.AddRange(entityChangeList); + } + return result; } catch (DbUpdateConcurrencyException ex) @@ -97,9 +129,32 @@ namespace Volo.Abp.EntityFrameworkCore try { ChangeTracker.AutoDetectChangesEnabled = false; //TODO: Why this is needed? + + var auditLog = AuditingManager?.Current?.Log; + + List entityChangeList = null; + if (auditLog != null) + { + entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList()); + } + else + { + Logger.LogWarning("AuditingManager?.Current is null!"); + } + var changeReport = ApplyAbpConcepts(); + var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + await EntityChangeEventHelper.TriggerEventsAsync(changeReport); + + if (auditLog != null) + { + EntityHistoryHelper.UpdateChangeList(entityChangeList); + auditLog.EntityChanges.AddRange(entityChangeList); + Logger.LogDebug($"Added {entityChangeList.Count} entity changes to the current audit log"); + } + return result; } catch (DbUpdateConcurrencyException ex) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs new file mode 100644 index 0000000000..6a5630d65b --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Json; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Timing; +using Volo.Abp.Uow; + +namespace Volo.Abp.EntityFrameworkCore.EntityHistory +{ + public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency + { + public ILogger Logger { get; set; } + + protected IAuditingStore AuditingStore { get; } + protected IJsonSerializer JsonSerializer { get; } + protected AuditingOptions Options { get; } + + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IClock _clock; + + public EntityHistoryHelper( + IUnitOfWorkManager unitOfWorkManager, + IAuditingStore auditingStore, + IOptions options, + IClock clock, + IJsonSerializer jsonSerializer) + { + _unitOfWorkManager = unitOfWorkManager; + _clock = clock; + AuditingStore = auditingStore; + JsonSerializer = jsonSerializer; + Options = options.Value; + + Logger = NullLogger.Instance; + } + + public virtual List CreateChangeList(ICollection entityEntries) + { + //TODO: Check if auditing disabled (on at somewhere else)? + + var list = new List(); + + foreach (var entry in entityEntries) + { + if (!ShouldSaveEntityHistory(entry)) + { + continue; + } + + var entityChange = CreateEntityChangeOrNull(entry); + if (entityChange == null) + { + continue; + } + + list.Add(entityChange); + } + + return list; + } + + [CanBeNull] + private EntityChangeInfo CreateEntityChangeOrNull(EntityEntry entityEntry) + { + var entity = entityEntry.Entity; + + EntityChangeType changeType; + switch (entityEntry.State) + { + case EntityState.Added: + changeType = EntityChangeType.Created; + break; + case EntityState.Deleted: + changeType = EntityChangeType.Deleted; + break; + case EntityState.Modified: + changeType = IsDeleted(entityEntry) ? EntityChangeType.Deleted : EntityChangeType.Updated; + break; + case EntityState.Detached: + case EntityState.Unchanged: + default: + return null; + } + + var entityId = GetEntityId(entity); + if (entityId == null && changeType != EntityChangeType.Created) + { + return null; + } + + var entityType = entity.GetType(); + var entityChange = new EntityChangeInfo + { + ChangeType = changeType, + EntityEntry = entityEntry, + EntityId = entityId, + EntityTypeFullName = entityType.FullName, + PropertyChanges = GetPropertyChanges(entityEntry), + TenantId = GetTenantId(entity) + }; + + return entityChange; + } + + protected virtual Guid? GetTenantId(object entity) + { + if (!(entity is IMultiTenant multiTenantEntity)) + { + return null; + } + + return multiTenantEntity.TenantId; + } + + private DateTime GetChangeTime(EntityChangeInfo entityChange) + { + var entity = entityChange.EntityEntry.As().Entity; + switch (entityChange.ChangeType) + { + case EntityChangeType.Created: + return (entity as IHasCreationTime)?.CreationTime ?? _clock.Now; + case EntityChangeType.Deleted: + return (entity as IHasDeletionTime)?.DeletionTime ?? _clock.Now; + case EntityChangeType.Updated: + return (entity as IHasModificationTime)?.LastModificationTime ?? _clock.Now; + default: + throw new AbpException($"Unknown {nameof(EntityChangeInfo)}: {entityChange}"); + } + } + + 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(","); + } + + /// + /// Gets the property changes for this entry. + /// + private List GetPropertyChanges(EntityEntry entityEntry) + { + var propertyChanges = new List(); + var properties = entityEntry.Metadata.GetProperties(); + var isCreated = IsCreated(entityEntry); + var isDeleted = IsDeleted(entityEntry); + + foreach (var property in properties) + { + var propertyEntry = entityEntry.Property(property.Name); + if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted)) + { + propertyChanges.Add(new EntityPropertyChangeInfo + { + NewValue = isDeleted ? null : JsonSerializer.Serialize(propertyEntry.CurrentValue).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), + OriginalValue = isCreated ? null : JsonSerializer.Serialize(propertyEntry.OriginalValue).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), + PropertyName = property.Name, + PropertyTypeFullName = property.ClrType.FullName, + TenantId = GetTenantId(entityEntry.Entity) + }); + } + } + + return propertyChanges; + } + + private bool IsCreated(EntityEntry entityEntry) + { + return entityEntry.State == EntityState.Added; + } + + private bool IsDeleted(EntityEntry entityEntry) + { + if (entityEntry.State == EntityState.Deleted) + { + return true; + } + + var entity = entityEntry.Entity; + return entity is ISoftDelete && entity.As().IsDeleted; + } + + private bool ShouldSaveEntityHistory(EntityEntry entityEntry, bool defaultValue = false) + { + if (entityEntry.State == EntityState.Detached || + entityEntry.State == EntityState.Unchanged) + { + return false; + } + + if (Options.IgnoredTypes.Any(t => t.IsInstanceOfType(entityEntry.Entity))) + { + return false; + } + + var entityType = entityEntry.Entity.GetType(); + if (!EntityHelper.IsEntity(entityType)) + { + return false; + } + + if (!entityType.IsPublic) + { + return false; + } + + if (entityType.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) + { + return true; + } + + if (entityType.GetTypeInfo().IsDefined(typeof(DisableAuditingAttribute), true)) + { + return false; + } + + var properties = entityEntry.Metadata.GetProperties(); + if (properties.Any(p => p.PropertyInfo?.IsDefined(typeof(AuditedAttribute)) ?? false)) + { + return true; + } + + return defaultValue; + } + + private bool ShouldSavePropertyHistory(PropertyEntry propertyEntry, bool defaultValue) + { + if (propertyEntry.Metadata.Name == "Id") + { + return false; + } + + var propertyInfo = propertyEntry.Metadata.PropertyInfo; + if (propertyInfo != null && propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + return false; + } + + var entityType = propertyEntry.EntityEntry.Entity.GetType(); + if (entityType.GetTypeInfo().IsDefined(typeof(DisableAuditingAttribute), true)) + { + if (propertyInfo == null || !propertyInfo.IsDefined(typeof(AuditedAttribute), true)) + { + return false; + } + } + + var isModified = !(propertyEntry.OriginalValue?.Equals(propertyEntry.CurrentValue) ?? propertyEntry.CurrentValue == null); + if (isModified) + { + return true; + } + + return defaultValue; + } + + /// + /// Updates change time, entity id and foreign keys after SaveChanges is called. + /// + public void UpdateChangeList(List entityChanges) + { + foreach (var entityChange in entityChanges) + { + /* Update change time */ + + entityChange.ChangeTime = GetChangeTime(entityChange); + + /* Update entity id */ + + var entityEntry = entityChange.EntityEntry.As(); + entityChange.EntityId = GetEntityId(entityEntry.Entity); + + /* Update foreign keys */ + + var foreignKeys = entityEntry.Metadata.GetForeignKeys(); + + foreach (var foreignKey in foreignKeys) + { + foreach (var property in foreignKey.Properties) + { + var propertyEntry = entityEntry.Property(property.Name); + var propertyChange = entityChange.PropertyChanges.FirstOrDefault(pc => pc.PropertyName == property.Name); + + if (propertyChange == null) + { + if (!(propertyEntry.OriginalValue?.Equals(propertyEntry.CurrentValue) ?? propertyEntry.CurrentValue == null)) + { + // Add foreign key + entityChange.PropertyChanges.Add(new EntityPropertyChangeInfo + { + NewValue = JsonSerializer.Serialize(propertyEntry.CurrentValue), + OriginalValue = JsonSerializer.Serialize(propertyEntry.OriginalValue), + PropertyName = property.Name, + PropertyTypeFullName = property.ClrType.FullName + }); + } + + continue; + } + + if (propertyChange.OriginalValue == propertyChange.NewValue) + { + var newValue = JsonSerializer.Serialize(propertyEntry.CurrentValue); + if (newValue == propertyChange.NewValue) + { + // No change + entityChange.PropertyChanges.Remove(propertyChange); + } + else + { + // Update foreign key + propertyChange.NewValue = newValue.TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength); + } + } + } + } + } + } + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs new file mode 100644 index 0000000000..041a9874a1 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Volo.Abp.Auditing; + +namespace Volo.Abp.EntityFrameworkCore.EntityHistory +{ + public interface IEntityHistoryHelper + { + List CreateChangeList(ICollection entityEntries); + + void UpdateChangeList(List entityChanges); + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs new file mode 100644 index 0000000000..f412f34d1d --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Volo.Abp.Auditing; + +namespace Volo.Abp.EntityFrameworkCore.EntityHistory +{ + public class NullEntityHistoryHelper : IEntityHistoryHelper + { + public static NullEntityHistoryHelper Instance { get; } = new NullEntityHistoryHelper(); + + private NullEntityHistoryHelper() + { + + } + + public List CreateChangeList(ICollection entityEntries) + { + return new List(); + } + + public void UpdateChangeList(List entityChanges) + { + + } + } +} \ No newline at end of file