public Task <DbSaveChangesResult> SavedChangesAsync( IEnumerable <IHookedEntity> entries, HookImportance minHookImportance = HookImportance.Normal, CancellationToken cancelToken = default) { return(Task.FromResult(DbSaveChangesResult.Empty)); }
public static HookMetadata Create <THook, TContext>(Type hookedType, HookImportance importance = HookImportance.Normal) where THook : IDbSaveHook where TContext : DbContext { Guard.NotNull(hookedType, nameof(hookedType)); return(new HookMetadata { ImplType = typeof(THook), DbContextType = typeof(TContext), HookedType = hookedType, Importance = importance }); }
public async Task <DbSavingChangesResult> SavingChangesAsync( IEnumerable <IHookedEntity> entries, HookImportance minHookImportance = HookImportance.Normal, CancellationToken cancelToken = default) { Guard.NotNull(entries, nameof(entries)); var anyStateChanged = false; if (!entries.Any() || !_saveHooks.Any()) { return(DbSavingChangesResult.Empty); } var processedHooks = new Multimap <IDbSaveHook, IHookedEntity>(); foreach (var entry in entries) { var e = entry; // Prevents access to modified closure if (cancelToken.IsCancellationRequested) { continue; } if (HandledAlready(e, HookStage.PreSave)) { // Prevent repetitive hooking of the same entity/state/pre combination within a single request continue; } var hooks = GetSaveHookInstancesFor(e, HookStage.PreSave, minHookImportance); foreach (var hook in hooks) { // call hook try { Logger.Debug("PRE save hook: {0}, State: {1}, Entity: {2}", hook.GetType().Name, e.InitialState, e.Entity.GetType().Name); var result = await hook.OnBeforeSaveAsync(e, cancelToken); if (result == HookResult.Ok) { processedHooks.Add(hook, e); } else if (result == HookResult.Void) { RegisterVoidHook(hook, e, HookStage.PreSave); } } catch (Exception ex) when(ex is NotImplementedException || ex is NotSupportedException) { RegisterVoidHook(hook, e, HookStage.PreSave); } catch (Exception ex) { Logger.Error(ex, "PreSaveHook exception ({0})", hook.GetType().FullName); } // change state if applicable if (e.HasStateChanged) { e.InitialState = e.State; anyStateChanged = true; } } } foreach (var hook in processedHooks) { await hook.Key.OnBeforeSaveCompletedAsync(hook.Value, cancelToken); } return(new DbSavingChangesResult(processedHooks.Keys, anyStateChanged) { Entries = entries }); }
private IEnumerable <IDbSaveHook> GetSaveHookInstancesFor(IHookedEntity entry, HookStage stage, HookImportance minHookImportance) { if (entry.EntityType == null) { return(Enumerable.Empty <IDbSaveHook>()); } IEnumerable <IDbSaveHook> hooks; // For request cache lookup var requestKey = new RequestHookKey(entry, stage, minHookImportance); if (_hooksRequestCache.ContainsKey(requestKey)) { hooks = _hooksRequestCache[requestKey]; } else { hooks = _saveHooks // Reduce by data context types .Where(x => x.Metadata.DbContextType.IsAssignableFrom(entry.DbContext.GetType())) // Reduce by entity types which can be processed by this hook .Where(x => x.Metadata.HookedType.IsAssignableFrom(entry.EntityType)) // Only include hook types with Importance >= minHookImportance .Where(x => x.Metadata.Importance >= minHookImportance) // Exclude void hooks (hooks known to be useless for the current EntityType/State/Stage combination) .Where(x => !_voidHooks.Contains(new HookKey(x.Metadata.ImplType, entry, stage))) // Apply sort .OrderBy(x => x.Metadata.Order) // Get the hook instance .Select(x => x.Value) // Make array .ToArray(); _hooksRequestCache.AddRange(requestKey, hooks); } return(hooks); }
public ImportantAttribute(HookImportance importance) { Importance = importance; }
/// <summary> /// Creates a scope in which a DbContext instance behaves differently. /// The behaviour is resetted on disposal of the scope to what it was before. /// </summary> /// <param name="ctx">The context instance to change behavior for.</param> /// <param name="deferCommit"> /// Suppresses the execution of <see cref="DbContext.SaveChanges()"/> / <see cref="DbContext.SaveChangesAsync(CancellationToken)"/> /// until this instance is disposed or <see cref="Commit()"/> / <see cref="CommitAsync(CancellationToken)"/> is called explicitly. /// </param> /// <param name="retainConnection"> /// Opens connection and retains it until disposal. May increase load/save performance in large scopes. /// </param> public DbContextScope(HookingDbContext ctx, bool?autoDetectChanges = null, bool?lazyLoading = null, bool?forceNoTracking = null, bool?deferCommit = false, bool retainConnection = false, HookImportance?minHookImportance = null, CascadeTiming?cascadeDeleteTiming = null, CascadeTiming?deleteOrphansTiming = null, bool?autoTransactions = null) { Guard.NotNull(ctx, nameof(ctx)); var changeTracker = ctx.ChangeTracker; _ctx = ctx; _autoDetectChangesEnabled = changeTracker.AutoDetectChangesEnabled; _minHookImportance = ctx.MinHookImportance; _suppressCommit = ctx.SuppressCommit; _lazyLoadingEnabled = changeTracker.LazyLoadingEnabled; _queryTrackingBehavior = changeTracker.QueryTrackingBehavior; _cascadeDeleteTiming = changeTracker.CascadeDeleteTiming; _deleteOrphansTiming = changeTracker.DeleteOrphansTiming; _autoTransactionEnabled = ctx.Database.AutoTransactionsEnabled; _retainConnection = retainConnection; if (autoDetectChanges.HasValue) { changeTracker.AutoDetectChangesEnabled = autoDetectChanges.Value; } if (minHookImportance.HasValue) { ctx.MinHookImportance = minHookImportance.Value; } if (lazyLoading.HasValue) { changeTracker.LazyLoadingEnabled = lazyLoading.Value; } if (forceNoTracking == true) { changeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } if (deferCommit.HasValue) { ctx.SuppressCommit = deferCommit.Value; } if (cascadeDeleteTiming.HasValue) { changeTracker.CascadeDeleteTiming = cascadeDeleteTiming.Value; } if (deleteOrphansTiming.HasValue) { changeTracker.DeleteOrphansTiming = deleteOrphansTiming.Value; } if (autoTransactions.HasValue) { ctx.Database.AutoTransactionsEnabled = autoTransactions.Value; } if (retainConnection) { ctx.Database.OpenConnection(); } }