EntityHistoryHelper.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.Entity;
  4. using System.Data.Entity.Core;
  5. using System.Data.Entity.Core.Metadata.Edm;
  6. using System.Data.Entity.Core.Objects;
  7. using System.Data.Entity.Infrastructure;
  8. using System.Linq;
  9. using System.Reflection;
  10. using System.Threading.Tasks;
  11. using System.Transactions;
  12. using Abp.Auditing;
  13. using Abp.Dependency;
  14. using Abp.Domain.Entities;
  15. using Abp.Domain.Entities.Auditing;
  16. using Abp.Domain.Uow;
  17. using Abp.EntityHistory;
  18. using Abp.Events.Bus.Entities;
  19. using Abp.Extensions;
  20. using Abp.Json;
  21. using Abp.Runtime.Session;
  22. using Abp.Timing;
  23. using Castle.Core.Logging;
  24. using IwbZero.EntityHistory.Extensions;
  25. using JetBrains.Annotations;
  26. namespace IwbZero.EntityHistory
  27. {
  28. public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
  29. {
  30. public ILogger Logger { get; set; }
  31. public IAbpSession AbpSession { get; set; }
  32. public IClientInfoProvider ClientInfoProvider { get; set; }
  33. public IEntityChangeSetReasonProvider EntityChangeSetReasonProvider { get; set; }
  34. public IEntityHistoryStore EntityHistoryStore { get; set; }
  35. private readonly IEntityHistoryConfiguration _configuration;
  36. private readonly IUnitOfWorkManager _unitOfWorkManager;
  37. private bool IsEntityHistoryEnabled
  38. {
  39. get
  40. {
  41. if (!_configuration.IsEnabled)
  42. {
  43. return false;
  44. }
  45. if (!_configuration.IsEnabledForAnonymousUsers && (AbpSession?.UserId == null))
  46. {
  47. return false;
  48. }
  49. return true;
  50. }
  51. }
  52. public EntityHistoryHelper(
  53. IEntityHistoryConfiguration configuration,
  54. IUnitOfWorkManager unitOfWorkManager)
  55. {
  56. _configuration = configuration;
  57. _unitOfWorkManager = unitOfWorkManager;
  58. AbpSession = NullAbpSession.Instance;
  59. Logger = NullLogger.Instance;
  60. ClientInfoProvider = NullClientInfoProvider.Instance;
  61. EntityChangeSetReasonProvider = NullEntityChangeSetReasonProvider.Instance;
  62. EntityHistoryStore = NullEntityHistoryStore.Instance;
  63. }
  64. public virtual EntityChangeSet CreateEntityChangeSet(DbContext context)
  65. {
  66. var changeSet = new EntityChangeSet
  67. {
  68. Reason = EntityChangeSetReasonProvider.Reason.TruncateWithPostfix(EntityChangeSet.MaxReasonLength),
  69. // Fill "who did this change"
  70. BrowserInfo = ClientInfoProvider.BrowserInfo.TruncateWithPostfix(EntityChangeSet.MaxBrowserInfoLength),
  71. ClientIpAddress = ClientInfoProvider.ClientIpAddress.TruncateWithPostfix(EntityChangeSet.MaxClientIpAddressLength),
  72. ClientName = ClientInfoProvider.ComputerName.TruncateWithPostfix(EntityChangeSet.MaxClientNameLength),
  73. ImpersonatorTenantId = AbpSession.ImpersonatorTenantId,
  74. ImpersonatorUserId = AbpSession.ImpersonatorUserId,
  75. TenantId = AbpSession.TenantId,
  76. UserId = AbpSession.UserId
  77. };
  78. if (!IsEntityHistoryEnabled)
  79. {
  80. return changeSet;
  81. }
  82. var objectContext = ((IObjectContextAdapter)context).ObjectContext;
  83. var relationshipChanges = objectContext.ObjectStateManager
  84. .GetObjectStateEntries(EntityState.Added | EntityState.Deleted)
  85. .Where(state => state.IsRelationship)
  86. .ToList();
  87. foreach (var entityEntry in context.ChangeTracker.Entries())
  88. {
  89. var shouldSaveEntityHistory = ShouldSaveEntityHistory(entityEntry);
  90. if (!shouldSaveEntityHistory && !entityEntry.HasAuditedProperties())
  91. {
  92. continue;
  93. }
  94. var entityType = GetEntityType(objectContext, entityEntry.GetEntityBaseType());
  95. var entityChange = CreateEntityChange(entityEntry, entityType);
  96. if (entityChange == null)
  97. {
  98. continue;
  99. }
  100. var entitySet = GetEntitySet(objectContext, entityType);
  101. var propertyChanges = new List<EntityPropertyChange>();
  102. propertyChanges.AddRange(GetPropertyChanges(entityEntry, entityType, entitySet, shouldSaveEntityHistory));
  103. propertyChanges.AddRange(GetRelationshipChanges(entityEntry, entityType, entitySet, relationshipChanges, shouldSaveEntityHistory));
  104. if (!shouldSaveEntityHistory && propertyChanges.Count == 0)
  105. {
  106. continue;
  107. }
  108. entityChange.PropertyChanges = propertyChanges;
  109. changeSet.EntityChanges.Add(entityChange);
  110. }
  111. return changeSet;
  112. }
  113. public virtual async Task SaveAsync(DbContext context, EntityChangeSet changeSet)
  114. {
  115. if (!IsEntityHistoryEnabled)
  116. {
  117. return;
  118. }
  119. if (changeSet.EntityChanges.Count == 0)
  120. {
  121. return;
  122. }
  123. UpdateChangeSet(context, changeSet);
  124. using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.Suppress))
  125. {
  126. await EntityHistoryStore.SaveAsync(changeSet);
  127. await uow.CompleteAsync();
  128. }
  129. }
  130. [CanBeNull]
  131. protected virtual string GetEntityId(DbEntityEntry entityEntry, EntityType entityType)
  132. {
  133. var primaryKey = entityType.KeyProperties.First();
  134. return entityEntry.Property(primaryKey.Name)?.GetNewValue()?.ToJsonString();
  135. }
  136. [CanBeNull]
  137. private EntityChange CreateEntityChange(DbEntityEntry entityEntry, EntityType entityType)
  138. {
  139. EntityChangeType changeType;
  140. switch (entityEntry.State)
  141. {
  142. case EntityState.Added:
  143. changeType = EntityChangeType.Created;
  144. break;
  145. case EntityState.Deleted:
  146. changeType = EntityChangeType.Deleted;
  147. break;
  148. case EntityState.Modified:
  149. changeType = entityEntry.IsDeleted() ? EntityChangeType.Deleted : EntityChangeType.Updated;
  150. break;
  151. case EntityState.Detached:
  152. case EntityState.Unchanged:
  153. default:
  154. Logger.Error("Unexpected EntityState!");
  155. return null;
  156. }
  157. var entityId = GetEntityId(entityEntry, entityType);
  158. if (entityId == null && changeType != EntityChangeType.Created)
  159. {
  160. Logger.Error("Unexpected null value for entityId!");
  161. return null;
  162. }
  163. var entityChange = new EntityChange
  164. {
  165. ChangeType = changeType,
  166. EntityEntry = entityEntry, // [NotMapped]
  167. EntityId = entityId,
  168. EntityTypeFullName = entityEntry.GetEntityBaseType().FullName,
  169. TenantId = AbpSession.TenantId
  170. };
  171. return entityChange;
  172. }
  173. private DateTime GetChangeTime(EntityChange entityChange)
  174. {
  175. var entity = entityChange.EntityEntry.As<DbEntityEntry>().Entity;
  176. switch (entityChange.ChangeType)
  177. {
  178. case EntityChangeType.Created:
  179. return (entity as IHasCreationTime)?.CreationTime ?? Clock.Now;
  180. case EntityChangeType.Deleted:
  181. return (entity as IHasDeletionTime)?.DeletionTime ?? Clock.Now;
  182. case EntityChangeType.Updated:
  183. return (entity as IHasModificationTime)?.LastModificationTime ?? Clock.Now;
  184. default:
  185. Logger.Error("Unexpected EntityState!");
  186. return Clock.Now;
  187. }
  188. }
  189. private EntityType GetEntityType(ObjectContext context, Type entityType, bool useClrType = true)
  190. {
  191. var metadataWorkspace = context.MetadataWorkspace;
  192. if (useClrType)
  193. {
  194. /* Get the mapping between Clr types in OSpace */
  195. var objectItemCollection = ((ObjectItemCollection)metadataWorkspace.GetItemCollection(DataSpace.OSpace));
  196. return metadataWorkspace
  197. .GetItems<EntityType>(DataSpace.OSpace)
  198. .Single(e => objectItemCollection.GetClrType(e) == entityType);
  199. }
  200. else
  201. {
  202. return metadataWorkspace
  203. .GetItems<EntityType>(DataSpace.CSpace)
  204. .Single(e => e.Name == entityType.Name);
  205. }
  206. }
  207. private EntitySet GetEntitySet(ObjectContext context, EntityType entityType)
  208. {
  209. var metadataWorkspace = context.MetadataWorkspace;
  210. /* Get the mapping between entity set/type in CSpace */
  211. return metadataWorkspace
  212. .GetItems<EntityContainer>(DataSpace.CSpace)
  213. .Single()
  214. .EntitySets
  215. .Single(e => e.ElementType.Name == entityType.Name);
  216. }
  217. /// <summary>
  218. /// Gets the property changes for this entry.
  219. /// </summary>
  220. private ICollection<EntityPropertyChange> GetPropertyChanges(DbEntityEntry entityEntry, EntityType entityType, EntitySet entitySet, bool shouldSaveEntityHistory)
  221. {
  222. var propertyChanges = new List<EntityPropertyChange>();
  223. var propertyNames = entityType.Properties.Select(e => e.Name);
  224. var complexTypeProperties = entitySet.ElementType.Properties.Where(e => e.IsComplexType).ToList();
  225. var isCreated = entityEntry.IsCreated();
  226. var isDeleted = entityEntry.IsDeleted();
  227. foreach (var propertyName in propertyNames)
  228. {
  229. if (entityType.KeyProperties.Any(m => m.Name == propertyName))
  230. {
  231. continue;
  232. }
  233. var memberEntry = entityEntry.Member(propertyName);
  234. if (!(memberEntry is DbPropertyEntry))
  235. {
  236. continue;
  237. }
  238. var propertyEntry = memberEntry as DbPropertyEntry;
  239. var propertyInfo = propertyEntry.EntityEntry.GetPropertyInfo(propertyEntry.Name);
  240. if (ShouldSavePropertyHistory(propertyEntry, propertyInfo, complexTypeProperties, shouldSaveEntityHistory, isCreated || isDeleted))
  241. {
  242. propertyChanges.Add(new EntityPropertyChange
  243. {
  244. NewValue = isDeleted ? null : propertyEntry.GetNewValue().ToJsonString().TruncateWithPostfix(EntityPropertyChange.MaxValueLength),
  245. OriginalValue = isCreated ? null : propertyEntry.GetOriginalValue().ToJsonString().TruncateWithPostfix(EntityPropertyChange.MaxValueLength),
  246. PropertyName = propertyName,
  247. PropertyTypeFullName = propertyInfo.PropertyType.FullName,
  248. TenantId = AbpSession.TenantId
  249. });
  250. }
  251. }
  252. return propertyChanges;
  253. }
  254. /// <summary>
  255. /// Gets the property changes for this entry.
  256. /// </summary>
  257. private ICollection<EntityPropertyChange> GetRelationshipChanges(DbEntityEntry entityEntry, EntityType entityType, EntitySet entitySet, ICollection<ObjectStateEntry> relationshipChanges, bool shouldSaveEntityHistory)
  258. {
  259. var propertyChanges = new List<EntityPropertyChange>();
  260. var navigationProperties = entityType.NavigationProperties;
  261. var isCreated = entityEntry.IsCreated();
  262. var isDeleted = entityEntry.IsDeleted();
  263. // Filter out relationship changes that are irrelevant to current entry
  264. var entityRelationshipChanges = relationshipChanges
  265. .Where(change => change.EntitySet is AssociationSet)
  266. .Where(change => change.EntitySet.As<AssociationSet>()
  267. .AssociationSetEnds
  268. .Select(set => set.EntitySet.ElementType.FullName).Contains(entitySet.ElementType.FullName)
  269. )
  270. .ToList();
  271. var relationshipGroups = entityRelationshipChanges
  272. .SelectMany(change =>
  273. {
  274. var values = change.State == EntityState.Added ? change.CurrentValues : change.OriginalValues;
  275. var valuesChangeSet = new object[values.FieldCount];
  276. values.GetValues(valuesChangeSet);
  277. return valuesChangeSet
  278. .Select(value => value.As<EntityKey>())
  279. .Where(value => value.EntitySetName != entitySet.Name)
  280. .Select(value => new Tuple<string, EntityState, EntityKey>(change.EntitySet.Name, change.State, value));
  281. })
  282. .GroupBy(t => t.Item1);
  283. foreach (var relationship in relationshipGroups)
  284. {
  285. var relationshipName = relationship.Key;
  286. var navigationPropertyName = navigationProperties
  287. .Where(p => p.RelationshipType.Name == relationshipName)
  288. .Select(p => p.Name)
  289. .FirstOrDefault();
  290. if (navigationPropertyName == null)
  291. {
  292. Logger.ErrorFormat("Unable to find navigation property for relationship {0} in entity {1}", relationshipName, entityType.Name);
  293. continue;
  294. }
  295. var propertyInfo = entityEntry.GetPropertyInfo(navigationPropertyName);
  296. if (ShouldSaveRelationshipHistory(entityRelationshipChanges, propertyInfo, shouldSaveEntityHistory, isCreated || isDeleted))
  297. {
  298. var addedRelationship = relationship.FirstOrDefault(p => p.Item2 == EntityState.Added);
  299. var deletedRelationship = relationship.FirstOrDefault(p => p.Item2 == EntityState.Deleted);
  300. var newValue = addedRelationship?.Item3.EntityKeyValues.ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value);
  301. var oldValue = deletedRelationship?.Item3.EntityKeyValues.ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value);
  302. propertyChanges.Add(new EntityPropertyChange
  303. {
  304. NewValue = newValue?.ToJsonString().TruncateWithPostfix(EntityPropertyChange.MaxValueLength),
  305. OriginalValue = oldValue?.ToJsonString().TruncateWithPostfix(EntityPropertyChange.MaxValueLength),
  306. PropertyName = navigationPropertyName,
  307. PropertyTypeFullName = propertyInfo.PropertyType.FullName,
  308. TenantId = AbpSession.TenantId
  309. });
  310. }
  311. }
  312. return propertyChanges;
  313. }
  314. private bool ShouldSaveEntityHistory(DbEntityEntry entityEntry)
  315. {
  316. if (entityEntry.State == EntityState.Detached ||
  317. entityEntry.State == EntityState.Unchanged)
  318. {
  319. return false;
  320. }
  321. if (_configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(entityEntry.Entity)))
  322. {
  323. return false;
  324. }
  325. var entityType = entityEntry.GetEntityBaseType();
  326. if (!EntityHelper.IsEntity(entityType))
  327. {
  328. return false;
  329. }
  330. var shouldSaveEntityHistoryForType = ShouldSaveEntityHistoryForType(entityType);
  331. if (shouldSaveEntityHistoryForType.HasValue)
  332. {
  333. return shouldSaveEntityHistoryForType.Value;
  334. }
  335. return false;
  336. }
  337. private bool? ShouldSaveEntityHistoryForType(Type entityType)
  338. {
  339. if (!entityType.IsPublic)
  340. {
  341. return false;
  342. }
  343. if (entityType.GetTypeInfo().IsDefined(typeof(DisableAuditingAttribute), true))
  344. {
  345. return false;
  346. }
  347. if (entityType.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true))
  348. {
  349. return true;
  350. }
  351. if (_configuration.Selectors.Any(selector => selector.Predicate(entityType)))
  352. {
  353. return true;
  354. }
  355. return null;
  356. }
  357. private bool ShouldSavePropertyHistory(DbPropertyEntry propertyEntry, PropertyInfo propertyInfo, ICollection<EdmProperty> complexTypeProperties, bool shouldSaveEntityHistory, bool defaultValue)
  358. {
  359. var shouldSavePropertyHistoryForInfo = ShouldSavePropertyHistoryForInfo(propertyInfo, shouldSaveEntityHistory);
  360. if (shouldSavePropertyHistoryForInfo.HasValue)
  361. {
  362. return shouldSavePropertyHistoryForInfo.Value;
  363. }
  364. var isModified = false;
  365. if (propertyEntry is DbComplexPropertyEntry)
  366. {
  367. var complexProperty = complexTypeProperties.Single(t => t.Name == propertyInfo.Name);
  368. isModified = propertyEntry.As<DbComplexPropertyEntry>().HasChanged(complexProperty);
  369. }
  370. else
  371. {
  372. isModified = propertyEntry.HasChanged();
  373. }
  374. if (isModified)
  375. {
  376. return true;
  377. }
  378. return defaultValue;
  379. }
  380. private bool ShouldSaveRelationshipHistory(ICollection<ObjectStateEntry> relationshipChanges, PropertyInfo propertyInfo, bool shouldSaveEntityHistory, bool defaultValue)
  381. {
  382. var shouldSavePropertyHistoryForInfo = ShouldSavePropertyHistoryForInfo(propertyInfo, shouldSaveEntityHistory);
  383. if (shouldSavePropertyHistoryForInfo.HasValue)
  384. {
  385. return shouldSavePropertyHistoryForInfo.Value;
  386. }
  387. var isModified = relationshipChanges.Any(change => change.State == EntityState.Added || change.State == EntityState.Deleted);
  388. if (isModified)
  389. {
  390. return true;
  391. }
  392. return defaultValue;
  393. }
  394. private bool? ShouldSavePropertyHistoryForInfo(PropertyInfo propertyInfo, bool shouldSaveEntityHistory)
  395. {
  396. if (propertyInfo != null && propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
  397. {
  398. return false;
  399. }
  400. if (!shouldSaveEntityHistory)
  401. {
  402. // Should not save property history if property is not audited
  403. if (propertyInfo == null || !propertyInfo.IsDefined(typeof(AuditedAttribute), true))
  404. {
  405. return false;
  406. }
  407. }
  408. return null;
  409. }
  410. /// <summary>
  411. /// Updates change time, entity id and foreign keys after SaveChanges is called.
  412. /// </summary>
  413. private void UpdateChangeSet(DbContext context, EntityChangeSet changeSet)
  414. {
  415. foreach (var entityChange in changeSet.EntityChanges)
  416. {
  417. /* Update change time */
  418. entityChange.ChangeTime = GetChangeTime(entityChange);
  419. /* Update entity id */
  420. var entityEntry = entityChange.EntityEntry.As<DbEntityEntry>();
  421. var entityType = GetEntityType(context.As<IObjectContextAdapter>().ObjectContext, entityEntry.GetEntityBaseType(), useClrType: false);
  422. entityChange.EntityId = GetEntityId(entityEntry, entityType);
  423. /* Update foreign keys */
  424. var foreignKeys = entityType.NavigationProperties;
  425. foreach (var foreignKey in foreignKeys)
  426. {
  427. foreach (var property in foreignKey.GetDependentProperties())
  428. {
  429. var propertyEntry = entityEntry.Property(property.Name);
  430. var propertyChange = entityChange.PropertyChanges.FirstOrDefault(pc => pc.PropertyName == property.Name);
  431. //make sure test case cover post saving (for foreign key update)
  432. if (propertyChange == null)
  433. {
  434. if (propertyEntry.HasChanged())
  435. {
  436. var propertyInfo = entityEntry.GetPropertyInfo(property.Name);
  437. // Add foreign key
  438. entityChange.PropertyChanges.Add(new EntityPropertyChange
  439. {
  440. NewValue = propertyEntry.CurrentValue.ToJsonString(),
  441. OriginalValue = propertyEntry.OriginalValue.ToJsonString(),
  442. PropertyName = property.Name,
  443. PropertyTypeFullName = propertyInfo.PropertyType.FullName
  444. });
  445. }
  446. continue;
  447. }
  448. if (propertyChange.OriginalValue == propertyChange.NewValue)
  449. {
  450. var newValue = propertyEntry.GetNewValue().ToJsonString();
  451. if (newValue == propertyChange.NewValue)
  452. {
  453. // No change
  454. entityChange.PropertyChanges.Remove(propertyChange);
  455. }
  456. else
  457. {
  458. // Update foreign key
  459. propertyChange.NewValue = newValue.TruncateWithPostfix(EntityPropertyChange.MaxValueLength);
  460. }
  461. }
  462. }
  463. }
  464. }
  465. }
  466. }
  467. }