Implemented entity history

pull/395/head
Halil ibrahim Kalkan 7 years ago
parent a1af5081aa
commit f715ad6520

@ -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; }

@ -32,14 +32,14 @@ namespace Volo.Abp.Auditing
public Dictionary<string, object> ExtraProperties { get; }
public IList<EntityChangeInfo> EntityChanges { get; }
public List<EntityChangeInfo> EntityChanges { get; }
public AuditLogInfo()
{
Actions = new List<AuditLogActionInfo>();
Exceptions = new List<Exception>();
ExtraProperties = new Dictionary<string, object>();
EntityChanges = new List<EntityChangeInfo>();
EntityChanges = new List<EntityChangeInfo>();
}
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();
}

@ -17,7 +17,7 @@ namespace Volo.Abp.Auditing
public Guid? TenantId { get; set; }
public ICollection<EntityPropertyChangeInfo> PropertyChanges { get; set; }
public List<EntityPropertyChangeInfo> PropertyChanges { get; set; }
#region Not mapped

@ -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
{
/// <summary>
/// Maximum length of <see cref="PropertyName"/> property.
/// Value: 96.
/// </summary>
public const int MaxPropertyNameLength = 96;
/// <summary>
/// Maximum length of <see cref="NewValue"/> and <see cref="OriginalValue"/> properties.
/// Value: 512.
/// </summary>
public const int MaxValueLength = 512;
/// <summary>
/// Maximum length of <see cref="PropertyTypeFullName"/> property.
/// Value: 512.
/// </summary>
public const int MaxPropertyTypeFullNameLength = 192;
public Guid? TenantId { get; set; }
public virtual string NewValue { get; set; }
public virtual string OriginalValue { get; set; }

@ -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<AbpDbContext<TDbContext>> Logger { get; set; }
private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo
= typeof(AbpDbContext<TDbContext>)
.GetMethod(
@ -52,6 +61,8 @@ namespace Volo.Abp.EntityFrameworkCore
{
GuidGenerator = SimpleGuidGenerator.Instance;
EntityChangeEventHelper = NullEntityChangeEventHelper.Instance;
EntityHistoryHelper = NullEntityHistoryHelper.Instance;
Logger = NullLogger<AbpDbContext<TDbContext>>.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<EntityChangeInfo> 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<EntityChangeInfo> 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)

@ -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<EntityHistoryHelper> 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<AuditingOptions> options,
IClock clock,
IJsonSerializer jsonSerializer)
{
_unitOfWorkManager = unitOfWorkManager;
_clock = clock;
AuditingStore = auditingStore;
JsonSerializer = jsonSerializer;
Options = options.Value;
Logger = NullLogger<EntityHistoryHelper>.Instance;
}
public virtual List<EntityChangeInfo> CreateChangeList(ICollection<EntityEntry> entityEntries)
{
//TODO: Check if auditing disabled (on at somewhere else)?
var list = new List<EntityChangeInfo>();
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<EntityEntry>().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(",");
}
/// <summary>
/// Gets the property changes for this entry.
/// </summary>
private List<EntityPropertyChangeInfo> GetPropertyChanges(EntityEntry entityEntry)
{
var propertyChanges = new List<EntityPropertyChangeInfo>();
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<ISoftDelete>().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;
}
/// <summary>
/// Updates change time, entity id and foreign keys after SaveChanges is called.
/// </summary>
public void UpdateChangeList(List<EntityChangeInfo> entityChanges)
{
foreach (var entityChange in entityChanges)
{
/* Update change time */
entityChange.ChangeTime = GetChangeTime(entityChange);
/* Update entity id */
var entityEntry = entityChange.EntityEntry.As<EntityEntry>();
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);
}
}
}
}
}
}
}
}

@ -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<EntityChangeInfo> CreateChangeList(ICollection<EntityEntry> entityEntries);
void UpdateChangeList(List<EntityChangeInfo> entityChanges);
}
}

@ -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<EntityChangeInfo> CreateChangeList(ICollection<EntityEntry> entityEntries)
{
return new List<EntityChangeInfo>();
}
public void UpdateChangeList(List<EntityChangeInfo> entityChanges)
{
}
}
}
Loading…
Cancel
Save