EntityHistoryHelper.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. using System.Transactions;
  2. using Abp.Dependency;
  3. using Abp.Domain.Uow;
  4. using Abp.EntityHistory;
  5. using Abp.Events.Bus.Entities;
  6. using Abp.Extensions;
  7. using Abp.Json;
  8. using Abp.Reflection;
  9. using JetBrains.Annotations;
  10. using Microsoft.EntityFrameworkCore;
  11. using Microsoft.EntityFrameworkCore.ChangeTracking;
  12. using Microsoft.EntityFrameworkCore.Metadata;
  13. using VberZero.EntityHistory.Extensions;
  14. namespace VberZero.EntityHistory;
  15. public class EntityHistoryHelper : EntityHistoryHelperBase, IEntityHistoryHelper, ITransientDependency
  16. {
  17. public EntityHistoryHelper(
  18. IEntityHistoryConfiguration configuration,
  19. IUnitOfWorkManager unitOfWorkManager)
  20. : base(configuration, unitOfWorkManager)
  21. {
  22. }
  23. public virtual EntityChangeSet CreateEntityChangeSet(ICollection<EntityEntry> entityEntries)
  24. {
  25. var changeSet = new EntityChangeSet
  26. {
  27. Reason = EntityChangeSetReasonProvider.Reason.TruncateWithPostfix(EntityChangeSet.MaxReasonLength),
  28. // Fill "who did this change"
  29. BrowserInfo = ClientInfoProvider.BrowserInfo.TruncateWithPostfix(EntityChangeSet.MaxBrowserInfoLength),
  30. ClientIpAddress =
  31. ClientInfoProvider.ClientIpAddress.TruncateWithPostfix(EntityChangeSet.MaxClientIpAddressLength),
  32. ClientName = ClientInfoProvider.ComputerName.TruncateWithPostfix(EntityChangeSet.MaxClientNameLength),
  33. ImpersonatorTenantId = AbpSession.ImpersonatorTenantId,
  34. ImpersonatorUserId = AbpSession.ImpersonatorUserId,
  35. TenantId = AbpSession.TenantId,
  36. UserId = AbpSession.UserId
  37. };
  38. if (!IsEntityHistoryEnabled)
  39. {
  40. return changeSet;
  41. }
  42. foreach (var entityEntry in entityEntries)
  43. {
  44. var typeOfEntity = ProxyHelper.GetUnproxiedType(entityEntry.Entity);
  45. var shouldTrackEntity = IsTypeOfTrackedEntity(typeOfEntity);
  46. if (shouldTrackEntity.HasValue && !shouldTrackEntity.Value)
  47. {
  48. continue;
  49. }
  50. if (!IsTypeOfEntity(typeOfEntity) && !entityEntry.Metadata.IsOwned())
  51. {
  52. continue;
  53. }
  54. var shouldAuditEntity = IsTypeOfAuditedEntity(typeOfEntity);
  55. if (shouldAuditEntity.HasValue && !shouldAuditEntity.Value)
  56. {
  57. continue;
  58. }
  59. bool? shouldAuditOwnerEntity = null;
  60. bool? shouldAuditOwnerProperty = null;
  61. if (!shouldAuditEntity.HasValue && entityEntry.Metadata.IsOwned())
  62. {
  63. // Check if owner entity has auditing attribute
  64. var ownerForeignKey = entityEntry.Metadata.GetForeignKeys().First(fk => fk.IsOwnership);
  65. var ownerEntityType = ownerForeignKey.PrincipalEntityType.ClrType;
  66. shouldAuditOwnerEntity = IsTypeOfAuditedEntity(ownerEntityType);
  67. if (shouldAuditOwnerEntity.HasValue && !shouldAuditOwnerEntity.Value)
  68. {
  69. continue;
  70. }
  71. var ownerPropertyInfo = ownerForeignKey.PrincipalToDependent.PropertyInfo;
  72. shouldAuditOwnerProperty = IsAuditedPropertyInfo(ownerEntityType, ownerPropertyInfo);
  73. if (shouldAuditOwnerProperty.HasValue && !shouldAuditOwnerProperty.Value)
  74. {
  75. continue;
  76. }
  77. }
  78. var entityChange = CreateEntityChange(entityEntry);
  79. if (entityChange == null)
  80. {
  81. continue;
  82. }
  83. var isAuditableEntity = (shouldAuditEntity.HasValue && shouldAuditEntity.Value) ||
  84. (shouldAuditOwnerEntity.HasValue && shouldAuditOwnerEntity.Value) ||
  85. (shouldAuditOwnerProperty.HasValue && shouldAuditOwnerProperty.Value);
  86. var isTrackableEntity = shouldTrackEntity.HasValue && shouldTrackEntity.Value;
  87. var shouldSaveAuditedPropertiesOnly = !isAuditableEntity && !isTrackableEntity;
  88. var propertyChanges = GetPropertyChanges(entityEntry, shouldSaveAuditedPropertiesOnly);
  89. if (propertyChanges.Count == 0)
  90. {
  91. continue;
  92. }
  93. entityChange.PropertyChanges = propertyChanges;
  94. changeSet.EntityChanges.Add(entityChange);
  95. }
  96. return changeSet;
  97. }
  98. public virtual async Task SaveAsync(EntityChangeSet changeSet)
  99. {
  100. if (!IsEntityHistoryEnabled)
  101. {
  102. return;
  103. }
  104. UpdateChangeSet(changeSet);
  105. if (changeSet.EntityChanges.Count == 0)
  106. {
  107. return;
  108. }
  109. using var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress);
  110. await EntityHistoryStore.SaveAsync(changeSet);
  111. await uow.CompleteAsync();
  112. }
  113. public virtual void Save(EntityChangeSet changeSet)
  114. {
  115. if (!IsEntityHistoryEnabled)
  116. {
  117. return;
  118. }
  119. UpdateChangeSet(changeSet);
  120. if (changeSet.EntityChanges.Count == 0)
  121. {
  122. return;
  123. }
  124. using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress))
  125. {
  126. EntityHistoryStore.Save(changeSet);
  127. uow.Complete();
  128. }
  129. }
  130. protected virtual string GetEntityId(EntityEntry entry)
  131. {
  132. var primaryKeys = entry.Properties.Where(p => p.Metadata.IsPrimaryKey());
  133. return primaryKeys.First().CurrentValue?.ToJsonString();
  134. }
  135. [CanBeNull]
  136. private EntityChange CreateEntityChange(EntityEntry entityEntry)
  137. {
  138. var entityId = GetEntityId(entityEntry);
  139. var entityTypeFullName = ProxyHelper.GetUnproxiedType(entityEntry.Entity).FullName;
  140. EntityChangeType changeType;
  141. switch (entityEntry.State)
  142. {
  143. case EntityState.Added:
  144. changeType = EntityChangeType.Created;
  145. break;
  146. case EntityState.Deleted:
  147. changeType = EntityChangeType.Deleted;
  148. break;
  149. case EntityState.Modified:
  150. changeType = entityEntry.IsDeleted() ? EntityChangeType.Deleted : EntityChangeType.Updated;
  151. break;
  152. case EntityState.Detached:
  153. case EntityState.Unchanged:
  154. return null;
  155. default:
  156. Logger.ErrorFormat("Unexpected {0} - {1}", nameof(entityEntry.State), entityEntry.State);
  157. return null;
  158. }
  159. if (entityId == null && changeType != EntityChangeType.Created)
  160. {
  161. Logger.ErrorFormat("EntityChangeType {0} must have non-empty entity id", changeType);
  162. return null;
  163. }
  164. return new EntityChange
  165. {
  166. ChangeType = changeType,
  167. EntityEntry = entityEntry, // [NotMapped]
  168. EntityId = entityId,
  169. EntityTypeFullName = entityTypeFullName,
  170. TenantId = AbpSession.TenantId
  171. };
  172. }
  173. /// <summary>
  174. /// Gets the property changes for this entry.
  175. /// </summary>
  176. private ICollection<EntityPropertyChange> GetPropertyChanges(EntityEntry entityEntry,
  177. bool auditedPropertiesOnly)
  178. {
  179. var propertyChanges = new List<EntityPropertyChange>();
  180. var properties = entityEntry.Metadata.GetProperties();
  181. foreach (var property in properties)
  182. {
  183. if (property.IsPrimaryKey())
  184. {
  185. continue;
  186. }
  187. var shouldSaveProperty = property.IsShadowProperty() // i.e. property.PropertyInfo == null
  188. ? !auditedPropertiesOnly
  189. : IsAuditedPropertyInfo(property.PropertyInfo) ?? !auditedPropertiesOnly;
  190. if (shouldSaveProperty)
  191. {
  192. var propertyEntry = entityEntry.Property(property.Name);
  193. propertyChanges.Add(
  194. CreateEntityPropertyChange(
  195. propertyEntry.GetOriginalValue(),
  196. propertyEntry.GetNewValue(),
  197. property
  198. )
  199. );
  200. }
  201. }
  202. return propertyChanges;
  203. }
  204. /// <summary>
  205. /// Updates change time, entity id, Adds foreign keys, Removes/Updates property changes after SaveChanges is called.
  206. /// </summary>
  207. private void UpdateChangeSet(EntityChangeSet changeSet)
  208. {
  209. var entityChangesToRemove = new List<EntityChange>();
  210. foreach (var entityChange in changeSet.EntityChanges)
  211. {
  212. var entityEntry = entityChange.EntityEntry.As<EntityEntry>();
  213. var entityEntryType = ProxyHelper.GetUnproxiedType(entityEntry.Entity);
  214. var isAuditedEntity = IsTypeOfAuditedEntity(entityEntryType) == true;
  215. /* Update change time */
  216. entityChange.ChangeTime = GetChangeTime(entityChange.ChangeType, entityEntry.Entity);
  217. /* Update entity id */
  218. entityChange.EntityId = GetEntityId(entityEntry);
  219. /* Update property changes */
  220. var trackedPropertyNames = entityChange.PropertyChanges.Select(pc => pc.PropertyName).ToList();
  221. var additionalForeignKeys = entityEntry.Metadata.GetDeclaredReferencingForeignKeys()
  222. .Where(fk => trackedPropertyNames.Contains(fk.Properties[0].Name))
  223. .ToList();
  224. /* Add additional foreign keys from navigation properties */
  225. foreach (var foreignKey in additionalForeignKeys)
  226. {
  227. foreach (var property in foreignKey.Properties)
  228. {
  229. var shouldSaveProperty = property.IsShadowProperty()
  230. ? null
  231. : IsAuditedPropertyInfo(entityEntryType, property.PropertyInfo);
  232. if (shouldSaveProperty.HasValue && !shouldSaveProperty.Value)
  233. {
  234. continue;
  235. }
  236. var propertyEntry = entityEntry.Property(property.Name);
  237. var newValue = propertyEntry.GetNewValue()?.ToJsonString();
  238. var oldValue = propertyEntry.GetOriginalValue()?.ToJsonString();
  239. // Add foreign key
  240. entityChange.PropertyChanges.Add(CreateEntityPropertyChange(oldValue, newValue, property));
  241. }
  242. }
  243. /* Update/Remove property changes */
  244. var propertyChangesToRemove = new List<EntityPropertyChange>();
  245. foreach (var propertyChange in entityChange.PropertyChanges)
  246. {
  247. var propertyEntry = entityEntry.Property(propertyChange.PropertyName);
  248. // Take owner entity type if this is an owned entity
  249. var propertyEntityType = entityEntryType;
  250. if (entityEntry.Metadata.IsOwned())
  251. {
  252. var ownerForeignKey = entityEntry.Metadata.GetForeignKeys().First(fk => fk.IsOwnership);
  253. propertyEntityType = ownerForeignKey.PrincipalEntityType.ClrType;
  254. }
  255. var isAuditedProperty = propertyEntry.Metadata.IsShadowProperty() ||
  256. IsAuditedPropertyInfo(propertyEntityType,
  257. propertyEntry.Metadata.PropertyInfo) == true;
  258. propertyChange.SetNewValue(propertyEntry.GetNewValue()?.ToJsonString());
  259. if (!isAuditedProperty || propertyChange.IsValuesEquals())
  260. {
  261. // No change
  262. propertyChangesToRemove.Add(propertyChange);
  263. }
  264. }
  265. foreach (var propertyChange in propertyChangesToRemove)
  266. {
  267. entityChange.PropertyChanges.Remove(propertyChange);
  268. }
  269. if (!isAuditedEntity && entityChange.PropertyChanges.Count == 0)
  270. {
  271. entityChangesToRemove.Add(entityChange);
  272. }
  273. }
  274. foreach (var entityChange in entityChangesToRemove)
  275. {
  276. changeSet.EntityChanges.Remove(entityChange);
  277. }
  278. }
  279. private EntityPropertyChange CreateEntityPropertyChange(object oldValue, object newValue, IProperty property)
  280. {
  281. var entityPropertyChange = new EntityPropertyChange()
  282. {
  283. PropertyName = property.Name.TruncateWithPostfix(EntityPropertyChange.MaxPropertyNameLength),
  284. PropertyTypeFullName = property.ClrType.FullName.TruncateWithPostfix(
  285. EntityPropertyChange.MaxPropertyTypeFullNameLength
  286. ),
  287. TenantId = AbpSession.TenantId
  288. };
  289. entityPropertyChange.SetNewValue(newValue?.ToJsonString());
  290. entityPropertyChange.SetOriginalValue(oldValue?.ToJsonString());
  291. return entityPropertyChange;
  292. }
  293. }