/// <summary>
        /// Reverts pending changes made to the database context.
        /// </summary>
        /// <param name="context">
        /// The database context whose pending changes to revert.
        /// </param>
        /// <param name="cancellationToken">
        /// A token to monitor for cancellation requests.
        /// </param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public static async Task UndoChangesAsync(this IAuditedContext context, CancellationToken cancellationToken = default)
        {
            if (context.ChangeTracker.AutoDetectChangesEnabled)
            {
                context.ChangeTracker.DetectChanges();
            }

            foreach (var entry in context.ChangeTracker.Entries())
            {
                switch (entry.State)
                {
                case EntityState.Deleted:
                    entry.State = EntityState.Unchanged;
                    break;

                case EntityState.Modified:
                    await entry.ReloadAsync(cancellationToken);

                    break;

                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
                }
            }
        }
Exemple #2
0
        public static async Task <int> ConcurrencySave(this IAuditedContext context, int retryAttempts, ILogger logger, CancellationToken cancellationToken)
        {
            var validationErrors = context.GetValidationErrors();

            if (validationErrors.Any())
            {
                throw new ValidationException($"Validation errors:\n {string.Join(Environment.NewLine, validationErrors)}");
            }

            context.SetAuditingFields();

            bool successfulSave;
            var  currentAttempt = 1;

            do
            {
                try
                {
                    return(await context.SaveChangesAsync(cancellationToken));
                }
                catch (DbUpdateConcurrencyException exception) when(currentAttempt <= retryAttempts)
                {
                    logger.LogWarning(exception, $"Attempt {currentAttempt}/{retryAttempts} failed. Retrying...");
                    successfulSave = false;
                    currentAttempt++;
                }
            } while (!successfulSave);

            throw new InvalidOperationException("A serious issue has been encountered! Contact support immediately.");
        }
        /// <summary>
        /// Discards the prepared audit records. This method should be called
        /// when the actual changes could not be saved to the database.
        /// </summary>
        /// <param name="context">
        /// The database context whose changes were recorded.
        /// </param>
        /// <param name="cancellationToken">
        /// A token to monitor for cancellation requests.
        /// </param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public static async Task DiscardRecordedChangesAsync(this IAuditedContext context, CancellationToken cancellationToken = default)
        {
            if (!context.CanAudit())
            {
                return;
            }

            await Task.WhenAll(context.ChangeRecorders.Select(x => x.DiscardAsync(cancellationToken)));
        }
        /// <summary>
        /// Determines whether the database context is configured to support
        /// auditing.
        /// </summary>
        /// <param name="auditedContext">The database context.</param>
        /// <returns>
        /// <c>true</c> if the services required for auditing are set; otherwise,
        /// <c>false</c>.
        /// </returns>
        public static bool CanAudit(this IAuditedContext auditedContext)
        {
            if (auditedContext == null)
            {
                throw new ArgumentNullException(nameof(auditedContext));
            }

            return(auditedContext.ChangeRecorders?.Any() == true &&
                   auditedContext.Identity != null);
        }
Exemple #5
0
        public static void SetAuditingFields(this IAuditedContext context)
        {
            if (context.ContextScope == null)
            {
                throw new InvalidOperationException($"{nameof(context.ContextScope)} has not bet set. Cannot set audit fields in {nameof(IAuditedContext)}");
            }

            foreach (var stateAction in context.ContextScope.StateActions)
            {
                InvokeStateActionPerEntity(context.ChangeTracker.Entries().Where(entry => entry.State == stateAction.Key).ToList(), stateAction.Value);
            }
        }
        private static void RecordChange(this IAuditedContext context, EntityEntry entity, IAuditedEntity auditedEntity)
        {
            if (!context.CanAudit())
            {
                return;
            }

            switch (entity.State)
            {
            case EntityState.Added:
                foreach (var changeRecorder in context.ChangeRecorders)
                {
                    changeRecorder.RecordAdd(auditedEntity);
                }
                break;

            case EntityState.Deleted:
                foreach (var changeRecorder in context.ChangeRecorders)
                {
                    changeRecorder.RecordDelete(auditedEntity);
                }
                break;

            case EntityState.Modified:
                foreach (var property in entity.CurrentValues.Properties)
                {
                    var entry = entity.Property(property.Name);
                    // Note: do not change the line below into `!=`!
                    // `Equals(object, object)` calls an overridden Equals,
                    // whereas `object != object` does not.
                    if (entry.IsModified && !Equals(entry.CurrentValue, entry.OriginalValue))
                    {
                        foreach (var changeRecorder in context.ChangeRecorders)
                        {
                            changeRecorder.RecordChange(auditedEntity, property.Name, entry.OriginalValue, entry.CurrentValue);
                        }
                    }
                }
                break;
            }
        }
        /// <summary>
        /// Records the currently tracked changes in the database context to all
        /// change recorders.
        /// </summary>
        /// <param name="context">
        /// The database context that contains the changes.
        /// </param>
        public static void RecordChanges(this IAuditedContext context)
        {
            if (context.ChangeTracker.AutoDetectChangesEnabled)
            {
                context.ChangeTracker.DetectChanges();
            }

            foreach (var entity in context.ChangeTracker.Entries().Where(e => e.Entity is IAuditedEntity))
            {
                var auditedEntity = (IAuditedEntity)entity.Entity;
                switch (entity.State)
                {
                case EntityState.Added:
                    auditedEntity.Created   = DateTimeOffset.Now;
                    auditedEntity.CreatedBy = context.Identity?.Id;
                    break;

                case EntityState.Modified:
                    auditedEntity.LastModified   = DateTimeOffset.Now;
                    auditedEntity.LastModifiedBy = context.Identity?.Id;
                    break;
                }

                context.RecordChange(entity, auditedEntity);
            }

            if (context.CanAudit())
            {
                foreach (var changeRecorder in context.ChangeRecorders)
                {
                    if (changeRecorder is IHasParentContext parentContextRecorder)
                    {
                        parentContextRecorder.ParentContext = (IAuditContext)context;
                    }

                    changeRecorder.OnSavingChanges();
                }
            }
        }
Exemple #8
0
 public static ContextRepository <IAuditedContext> Create(IAuditedContext context)
 {
     return(new ContextRepository <IAuditedContext>(context, new Mock <ILogger <IAuditedContext> >().Object));
 }
Exemple #9
0
        public static IDictionary <object, ICollection <ValidationResult> > GetValidationErrors(this IAuditedContext context)
        {
            var validationErrors = new Dictionary <object, ICollection <ValidationResult> >();

            context.ChangeTracker.Entries().Where(entry => _entityStates.Contains(entry.State)).ToList().ForEach(entry =>
            {
                var ValidationResult = new List <ValidationResult>();

                if (!Validator.TryValidateObject(entry.Entity, new ValidationContext(entry.Entity), ValidationResult, true))
                {
                    validationErrors.Add(entry.Entity, ValidationResult);
                }
            });

            return(validationErrors);
        }