mirror of https://github.com/abpframework/abp.git
8 changed files with 481 additions and 10 deletions
@ -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…
Reference in new issue