mirror of https://github.com/abpframework/abp
parent
a1af5081aa
commit
f715ad6520
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue