Example #1
0
        /// <summary>
        /// This runs the events before and after the base SaveChangesAsync method is run
        /// </summary>
        /// <param name="context">The current DbContext</param>
        /// <param name="getTrackedEntities">A function to get the tracked entities</param>
        /// <param name="callBaseSaveChangesAsync">A function that is linked to the base SaveChangesAsync in your DbContext</param>
        /// <returns>Returns the status with the numUpdated number from SaveChanges</returns>
        public async Task <IStatusGeneric <int> > RunEventsBeforeAfterSaveChangesAsync(DbContext context, Func <IEnumerable <EntityEntry <EntityEvents> > > getTrackedEntities,
                                                                                       Func <Task <int> > callBaseSaveChangesAsync)
        {
            var status = new StatusGenericHandler <int>();

            status.CombineStatuses(RunBeforeSaveChangesEvents(getTrackedEntities));
            if (!status.IsValid)
            {
                return(status);
            }

            //Call SaveChangesAsync with catch for exception handler
            do
            {
                try
                {
                    status.SetResult(await callBaseSaveChangesAsync.Invoke().ConfigureAwait(false));
                    break; //This breaks out of the do/while
                }
                catch (Exception e)
                {
                    var exceptionStatus = _config.SaveChangesExceptionHandler?.Invoke(e, context);
                    if (exceptionStatus == null)
                    {
                        //This means the SaveChangesExceptionHandler doesn't cover this type of Concurrency Exception
                        throw;
                    }
                    //SaveChangesExceptionHandler ran, so combine its error into the outer status
                    status.CombineStatuses(exceptionStatus);
                }
                //If the SaveChangesExceptionHandler fixed the problem then we call SaveChanges again, but with the same exception catching.
            } while (status.IsValid);
            RunAfterSaveChangesEvents(getTrackedEntities);
            return(status);
        }
Example #2
0
        public async ValueTask <IStatusGeneric> RunDuringSaveChangesEventsAsync(DbContext context, bool postSaveChanges, bool allowAsync)
        {
            var status    = new StatusGenericHandler();
            var eventType = postSaveChanges
                ? BeforeDuringOrAfter.DuringSave
                : BeforeDuringOrAfter.DuringBeforeSave;

            foreach (var entityAndEvent in postSaveChanges ? _duringAfterEvents : _duringBeforeEvents)
            {
                if (allowAsync)
                {
                    status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync(
                                               entityAndEvent, 1, eventType, true)
                                           .ConfigureAwait(false));
                }
                else
                {
                    var findRunStatus =
                        _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, eventType, false);
                    if (!findRunStatus.IsCompleted)
                    {
                        throw new InvalidOperationException("Can only run sync tasks");
                    }
                    status.CombineStatuses(findRunStatus.Result);
                }
            }

            return(status);
        }
Example #3
0
        //NOTE: This had problems throwing an exception (don't know why - RunBeforeSaveChangesEventsAsync work!?).
        //Having it return an exception fixed it
        public async ValueTask RunAfterSaveChangesEventsAsync(DbContext context, bool allowAsync)
        {
            var status = new StatusGenericHandler();

            if (_config.NotUsingAfterSaveHandlers)
            {
                //Skip this stage if NotUsingAfterSaveHandlers is true
                return;
            }

            var eventsToRun = new List <EntityAndEvent>();

            foreach (var entityEntry in context.ChangeTracker.Entries <IEntityWithAfterSaveEvents>())
            {
                eventsToRun.AddRange(entityEntry.Entity.GetAfterSaveEventsThenClear()
                                     .Select(x => new EntityAndEvent(entityEntry.Entity, x))
                                     .Distinct());
            }

            foreach (var entityAndEvent in eventsToRun)
            {
                if (allowAsync)
                {
                    status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, BeforeDuringOrAfter.AfterSave, allowAsync)
                                           .ConfigureAwait(false));
                }
                else
                {
                    var findRunStatus =
                        _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, BeforeDuringOrAfter.AfterSave, allowAsync);
                    findRunStatus.CheckSyncValueTaskWorked();
                    status.CombineStatuses(findRunStatus.Result);
                }
            }
        }
Example #4
0
        public async ValueTask <IStatusGeneric> RunDuringSaveChangesEventsAsync(bool postSaveChanges, bool allowAsync)
        {
            var status = new StatusGenericHandler();

            if (_config.NotUsingDuringSaveHandlers)
            {
                //Skip this stage if NotUsingDuringSaveHandlers is true
                return(status);
            }

            var eventType = postSaveChanges
                ? BeforeDuringOrAfter.DuringSave
                : BeforeDuringOrAfter.DuringBeforeSave;

            foreach (var entityAndEvent in postSaveChanges ? _duringAfterEvents : _duringBeforeEvents)
            {
                if (allowAsync)
                {
                    status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync(
                                               entityAndEvent, 1, eventType, true)
                                           .ConfigureAwait(false));
                }
                else
                {
                    var findRunStatus =
                        _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, eventType, false);
                    findRunStatus.CheckSyncValueTaskWorked();
                    status.CombineStatuses(findRunStatus.Result);
                }
            }

            return(status);
        }
Example #5
0
        public IStatusGeneric NonStatusGenericNum(int i)
        {
            var status = new StatusGenericHandler();

            //add error and return immediately
            if (i <= 0)
            {
                return(status.AddError("input must be positive", nameof(i)));
            }

            //combine error from another non-StatusGeneric method and return if has errors
            status.CombineStatuses(NonStatusGenericString(i == 10 ? null : "good"));
            if (status.HasErrors)
            {
                return(status);
            }

            //combine errors from another generic status, keeping the status to extract later
            var stringStatus = StatusGenericNumReturnString(i);

            if (status.CombineStatuses(stringStatus).HasErrors)
            {
                return(status);
            }

            var stringResult = stringStatus.Result;

            //Other methods here

            return(status);
        }
        public async Task <IStatusGeneric <Order> >          //#A
        CreateOrderAndSaveAsync(PlaceOrderInDto dto)         //#B
        {
            var status = new StatusGenericHandler <Order>(); //#C

            if (!dto.AcceptTAndCs)                           //#D
            {
                return(status.AddError("You must accept the T&Cs to place an order."));
            }
            if (!dto.LineItems.Any())                 //#D
            {
                return(status.AddError("No items in your basket."));
            }

            var booksDict = await _dbAccess                            //#E
                            .FindBooksByIdsAsync                       //#E
                                (dto.LineItems.Select(x => x.BookId)); //#E

            var linesStatus = FormLineItemsWithErrorChecking           //#F
                                  (dto.LineItems, booksDict);          //#F

            if (status.CombineStatuses(linesStatus).HasErrors)         //#G
            {
                return(status);                                        //#G
            }
            var orderStatus = Order.CreateOrder(                       //#H
                dto.UserId, linesStatus.Result);                       //#H

            if (status.CombineStatuses(orderStatus).HasErrors)         //#I
            {
                return(status);                                        //#I
            }
            await _dbAccess.AddAndSave(orderStatus.Result);            //#J

            return(status.SetResult(orderStatus.Result));              //#K
        }
        /// <summary>
        /// This finds either sync or async handlers for the event and runs the handlers with the input event
        /// </summary>
        /// <param name="entityAndEvent"></param>
        /// <param name="loopCount">This gives the loop number for the RunBefore/AfterSaveChangesEvents</param>
        /// <param name="beforeDuringOrAfter">tells you what type of event to find/Run</param>
        /// <param name="allowAsync">true if async is allowed</param>
        /// <returns>Returns a Task containing the combined status from all the event handlers that ran</returns>
        public async ValueTask <IStatusGeneric> RunHandlersForEventAsync(EntityAndEvent entityAndEvent, int loopCount,
                                                                         BeforeDuringOrAfter beforeDuringOrAfter, bool allowAsync)
        {
            var status = new StatusGenericHandler
            {
                Message = "Successfully saved."
            };

            var handlersAndWrappers = _findHandlers.GetHandlers(entityAndEvent, beforeDuringOrAfter, allowAsync);

            foreach (var handlerWrapper in handlersAndWrappers)
            {
                LogEventHandlerRun(loopCount, beforeDuringOrAfter, handlerWrapper);
                if (beforeDuringOrAfter == BeforeDuringOrAfter.BeforeSave)
                {
                    var handlerStatus = handlerWrapper.IsAsync
                        ? await((BeforeSaveEventHandlerAsync)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler))
                                        .HandleAsync(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent).ConfigureAwait(false)
                        : ((BeforeSaveEventHandler)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler))
                                        .Handle(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent);
                    if (handlerStatus != null)
                    {
                        status.CombineStatuses(handlerStatus);
                    }
                }
                else if (beforeDuringOrAfter == BeforeDuringOrAfter.AfterSave)
                {
                    if (handlerWrapper.IsAsync)
                    {
                        await((AfterSaveEventHandlerAsync)Activator.CreateInstance(handlerWrapper.WrapperType,
                                                                                   handlerWrapper.EventHandler))
                        .HandleAsync(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent).ConfigureAwait(false);
                    }
                    else
                    {
                        ((AfterSaveEventHandler)Activator.CreateInstance(handlerWrapper.WrapperType,
                                                                         handlerWrapper.EventHandler))
                        .Handle(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent);
                    }
                }
                else
                {
                    //Its either of the during events

                    var handlerStatus = handlerWrapper.IsAsync
                        ? await((DuringSaveEventHandlerAsync)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler))
                                        .HandleAsync(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent, _uniqueValue)
                                        .ConfigureAwait(false)
                        : ((DuringSaveEventHandler)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler))
                                        .Handle(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent, _uniqueValue);
                    if (handlerStatus != null)
                    {
                        status.CombineStatuses(handlerStatus);
                    }
                }
            }

            return(status);
        }
Example #8
0
        private IStatusGeneric <int> //#A
        CallSaveChangesWithExceptionHandler
            (DbContext context,
            Func <int> callBaseSaveChanges)                //#B
        {
            var status = new StatusGenericHandler <int>(); //#C

            do                                             //#D
            {
                try
                {
                    int numUpdated = callBaseSaveChanges(); //#E
                    status.SetResult(numUpdated);           //#F
                    break;                                  //#F
                }
                catch (Exception e)                         //#G
                {
                    IStatusGeneric handlerStatus = null;    //#H
                    if (handlerStatus == null)              //#I
                    {
                        throw;                              //#I
                    }
                    status.CombineStatuses(handlerStatus);  //#J
                }
            } while (status.IsValid);                       //#K

            return(status);                                 //#L
        }
Example #9
0
        //NOTE: This had problems throwing an exception (don't know why - RunBeforeSaveChangesEventsAsync work!?).
        //Having it return an exception fixed it
        public async ValueTask <Exception> RunAfterSaveChangesEventsAsync(DbContext context, bool allowAsync)
        {
            var status = new StatusGenericHandler();

            if (_config.NotUsingAfterSaveHandlers)
            {
                //Skip this stage if NotUsingAfterSaveHandlers is true
                return(null);
            }

            var eventsToRun = new List <EntityAndEvent>();

            foreach (var entityEntry in context.ChangeTracker.Entries <IEntityWithAfterSaveEvents>())
            {
                eventsToRun.AddRange(entityEntry.Entity.GetAfterSaveEventsThenClear()
                                     .Select(x => new EntityAndEvent(entityEntry.Entity, x)));
            }

            foreach (var entityAndEvent in eventsToRun)
            {
                if (allowAsync)
                {
                    status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, BeforeDuringOrAfter.AfterSave, allowAsync)
                                           .ConfigureAwait(false));
                }
                else
                {
                    var findRunStatus =
                        _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, BeforeDuringOrAfter.AfterSave, allowAsync);
                    if (!findRunStatus.IsCompleted)
                    {
                        throw new InvalidOperationException("Can only run sync tasks");
                    }
                    status.CombineStatuses(findRunStatus.Result);
                }
            }

            return(null);
        }
Example #10
0
        //------------------------------------------
        // private methods

        private IStatusGeneric RunBeforeSaveChangesEvents(Func <IEnumerable <EntityEntry <EntityEvents> > > getTrackedEntities)
        {
            var status = new StatusGenericHandler();

            //This has to run until there are no new events, because one event might trigger another event
            bool shouldRunAgain;
            int  loopCount = 1;

            do
            {
                var eventsToRun = new List <EntityAndEvent>();
                foreach (var entity in getTrackedEntities.Invoke().Select(x => x.Entity))
                {
                    eventsToRun.AddRange(entity.GetBeforeSaveEventsThenClear()
                                         .Select(x => new EntityAndEvent(entity, x)));
                }

                shouldRunAgain = false;
                foreach (var entityAndEvent in eventsToRun)
                {
                    shouldRunAgain = true;
                    status.CombineStatuses(_findRunHandlers.RunHandlersForEvent(entityAndEvent, loopCount, true));
                    if (!status.IsValid && _config.StopOnFirstBeforeHandlerThatHasAnError)
                    {
                        break;
                    }
                }
                if (loopCount++ > _config.MaxTimesToLookForBeforeEvents)
                {
                    throw new GenericEventRunnerException(
                              $"The BeforeSave event loop exceeded the config's {nameof(GenericEventRunnerConfig.MaxTimesToLookForBeforeEvents)}" +
                              $" value of {_config.MaxTimesToLookForBeforeEvents}. This implies a circular sets of events. " +
                              "Look at EventsRunner Information logs for more information on what event handlers were running.",
                              eventsToRun.Last().CallingEntity, eventsToRun.Last().DomainEvent);
                }
            } while (shouldRunAgain && (status.IsValid || !_config.StopOnFirstBeforeHandlerThatHasAnError));

            if (!status.IsValid)
            {
                //If errors then clear any extra before/after events.
                //We need to do this to ensure another call to SaveChanges doesn't get the old events
                getTrackedEntities.Invoke().ToList().ForEach(x =>
                {
                    x.Entity.GetBeforeSaveEventsThenClear();
                    x.Entity.GetAfterSaveEventsThenClear();
                });
            }

            return(status);
        }
        public void TestGenericStatusCombineStatusesIsValidWithMessageOk()
        {
            //SETUP
            var status1 = new StatusGenericHandler();
            var status2 = new StatusGenericHandler();

            //ATTEMPT
            status1.Message = "My message";
            status2.CombineStatuses(status1);

            //VERIFY
            status2.IsValid.ShouldBeTrue();
            status2.Message.ShouldEqual("My message");
        }
        public void TestGenericStatusCombineStatusesWithErrorsOk()
        {
            //SETUP
            var status1 = new StatusGenericHandler();
            var status2 = new StatusGenericHandler();

            //ATTEMPT
            status1.AddError("This is an error.");
            status2.CombineStatuses(status1);

            //VERIFY
            status2.IsValid.ShouldBeFalse();
            status2.Errors.Single().ToString().ShouldEqual("This is an error.");
            status2.Message.ShouldEqual("Failed with 1 error");
        }
        public void TestGenericStatusHeaderCombineStatusesOk()
        {
            //SETUP
            var status1 = new StatusGenericHandler("MyClass");
            var status2 = new StatusGenericHandler("MyProp");

            //ATTEMPT
            status2.AddError("This is an error.");
            status1.CombineStatuses(status2);

            //VERIFY
            status1.IsValid.ShouldBeFalse();
            status1.Errors.Single().ToString().ShouldEqual("MyClass>MyProp: This is an error.");
            status1.Message.ShouldEqual("Failed with 1 error");
        }
        /// <summary>
        /// This is designed to add one DTO to an existing SpecificUseData
        /// </summary>
        /// <typeparam name="TDto">This should be the type of a class that has the <see cref="ILinkToEntity{TEntity}"/> applied to it</typeparam>
        /// <param name="utData"></param>
        /// <returns></returns>
        public static SpecificUseData AddSingleDto <TDto>(this SpecificUseData utData)
        {
            var status          = new StatusGenericHandler();
            var typesInAssembly = typeof(TDto).Assembly.GetTypes();
            var dtoRegister     = new RegisterOneDtoType(typeof(TDto), typesInAssembly, utData.PublicConfig);

            status.CombineStatuses(dtoRegister);
            if (!status.IsValid)
            {
                throw new InvalidOperationException($"SETUP FAILED with {status.Errors.Count} errors. Errors are:\n"
                                                    + status.GetAllErrors());
            }

            SetupDtosAndMappings.SetupMappingForDto(dtoRegister, utData.ReadProfile, utData.SaveProfile);
            return(utData);
        }
Example #15
0
        /// <summary>
        /// This finds the handlers for the event and runs the handlers with the input event
        /// </summary>
        /// <param name="entityAndEvent"></param>
        /// <param name="loopCount">This gives the loop number for the RunBefore/AfterSaveChangesEvents</param>
        /// <param name="beforeSave">true for BeforeSave, and false for AfterSave</param>
        /// <returns>Returns status with </returns>
        public IStatusGeneric RunHandlersForEvent(EntityAndEvent entityAndEvent, int loopCount, bool beforeSave)
        {
            var status = new StatusGenericHandler
            {
                Message = "Successfully saved."
            };

            var eventType        = entityAndEvent.DomainEvent.GetType();
            var handlerInterface = (beforeSave ? typeof(IBeforeSaveEventHandler <>) : typeof(IAfterSaveEventHandler <>))
                                   .MakeGenericType(eventType);
            var wrapperType = (beforeSave ? typeof(BeforeSaveHandler <>) : typeof(AfterSaveHandler <>))
                              .MakeGenericType(eventType);
            var handlers = _serviceProvider.GetServices(handlerInterface).ToList();

            var beforeAfter = beforeSave ? "BeforeSave" : "AfterSave";

            if (!handlers.Any())
            {
                _logger.LogError($"Missing handler for event of type {eventType.FullName} for {beforeAfter} event handler.");
                throw new GenericEventRunnerException(
                          $"Could not find a {beforeAfter} event handler for the event {eventType.Name}.",
                          entityAndEvent.CallingEntity, entityAndEvent.DomainEvent);
            }

            foreach (var handler in handlers)
            {
                _logger.LogInformation($"{beforeAfter[0]}{loopCount}: About to run a {beforeAfter} event handler {handler.GetType().FullName}.");
                if (beforeSave)
                {
                    var wrappedHandler = (BeforeSaveEventHandler)Activator.CreateInstance(wrapperType, handler);
                    var handlerStatus  = wrappedHandler.Handle(entityAndEvent.CallingEntity, entityAndEvent.DomainEvent);
                    if (handlerStatus != null)
                    {
                        status.CombineStatuses(handlerStatus);
                    }
                }
                else
                {
                    var wrappedHandler = (AfterSaveEventHandler)Activator.CreateInstance(wrapperType, handler);
                    wrappedHandler.Handle(entityAndEvent.CallingEntity, entityAndEvent.DomainEvent);
                }
            }

            return(status);
        }
Example #16
0
        private async Task <IStatusGeneric <int> > CallSaveChangesWithExceptionHandlerAsync(DbContext context, Func <Task <int> > callBaseSaveChangesAsync)
        {
            var status = new StatusGenericHandler <int>();

            do
            {
                try
                {
                    context.ChangeTracker.AutoDetectChangesEnabled = false;

                    status.SetResult(await callBaseSaveChangesAsync().ConfigureAwait(false));
                    break; //This breaks out of the do/while
                }
                catch (Exception e)
                {
                    IStatusGeneric exceptionStatus = null;
                    if (_config.ExceptionHandlerDictionary.TryGetValue(context.GetType(), out var exceptionHandler))
                    {
                        exceptionStatus = exceptionHandler(e, context);
                    }
                    if (exceptionStatus == null)
                    {
                        //This means the SaveChangesExceptionHandler doesn't cover this type of Concurrency Exception
                        throw;
                    }
                    //SaveChangesExceptionHandler ran, so combine its error into the outer status
                    status.CombineStatuses(exceptionStatus);
                }
                finally
                {
                    context.ChangeTracker.AutoDetectChangesEnabled = true;
                }

                //If the SaveChangesExceptionHandler fixed the problem then we call SaveChanges again, but with the same exception catching.
            } while (status.IsValid);

            return(status);
        }
Example #17
0
        /// <summary>
        /// This creates the order and, if successful clears the cookie
        /// </summary>
        /// <returns>Returns the OrderId, or zero if errors</returns>
        public async Task <IStatusGeneric <int> > PlaceOrderAndClearBasketAsync(bool acceptTAndCs)
        {
            var status = new StatusGenericHandler <int>();

            var checkoutService = new CheckoutCookieService(
                _basketCookie.GetValue());

            var bizStatus = await _placeOrder.CreateOrderAndSaveAsync(
                new PlaceOrderInDto(acceptTAndCs, checkoutService.UserId, checkoutService.LineItems));


            if (status.CombineStatuses(bizStatus).HasErrors)
            {
                return(status);
            }

            //successful so clear the cookie line items
            checkoutService.ClearAllLineItems();
            _basketCookie.AddOrUpdateCookie(
                checkoutService.EncodeForCookie());

            return(status.SetResult(bizStatus.Result.OrderId));
        }
        /// <summary>
        /// This converts the <see cref="ActionResult{T}"/> used in a create action into a GenericServices.IStatusGeneric
        /// </summary>
        /// <param name="actionResult"></param>
        /// <param name="routeName"></param>
        /// <param name="routeValues"></param>
        /// <param name="dto"></param>
        /// <returns>a status which is similar to the original status (errors might not be in the exact same form)</returns>
        public static IStatusGeneric CheckCreateResponse <T>(this ActionResult <T> actionResult, string routeName, object routeValues, T dto)
            where T : class
        {
            var testStatus = new StatusGenericHandler();
            var objResult  = (actionResult.Result as ObjectResult);

            if (objResult == null)
            {
                throw new NullReferenceException("Could not cast the response to ObjectResult");
            }
            var errorPart = objResult as BadRequestObjectResult;

            if (errorPart != null)
            {
                testStatus.Message = "Errors: " + string.Join("\n", ExtractErrors(errorPart));
                return(testStatus);
            }

            var createdAtRouteResult = objResult as CreatedAtRouteResult;

            if (createdAtRouteResult == null)
            {
                throw new NullReferenceException($"Could not cast the response value to CreatedAtRouteResult");
            }
            if (createdAtRouteResult.RouteName != routeName)
            {
                testStatus.AddError($"RouteName: expected {routeName}, found: {createdAtRouteResult.RouteName}");
            }
            if (createdAtRouteResult.Value as T != dto)
            {
                testStatus.AddError($"DTO: the returned DTO instance does not match the test DTO: expected {typeof(T).Name}, found: {createdAtRouteResult.Value.GetType().Name}");
            }
            testStatus.CombineStatuses(CompareRouteValues(createdAtRouteResult.RouteValues, routeValues));

            return(testStatus);
        }
Example #19
0
        /// <summary>
        /// This runs the events before, during, and after the base SaveChanges method is run
        /// </summary>
        /// <param name="context">The current DbContext</param>
        /// <param name="callBaseSaveChanges">A function that is linked to the base SaveChanges in your DbContext</param>
        /// <returns>Returns the status with the numUpdated number from SaveChanges</returns>
        public IStatusGeneric <int> RunEventsBeforeDuringAfterSaveChanges(DbContext context, Func <int> callBaseSaveChanges)
        {
            IStatusGeneric <int> RunTransactionWithDuringSaveChangesEvents()
            {
                var localStatus = new StatusGenericHandler <int>();

                //If there is a current transaction then use that, otherwise
                using var transaction = context.Database.CurrentTransaction == null
                    ? context.Database.BeginTransaction()
                    : null;

                var duringPreValueTask = _eachEventRunner.RunDuringSaveChangesEventsAsync(context, false, false);

                if (!duringPreValueTask.IsCompleted)
                {
                    throw new InvalidOperationException("Can only run sync tasks");
                }
                localStatus.CombineStatuses(duringPreValueTask.Result);

                var transactionSaveChanges = CallSaveChangesWithExceptionHandler(context, callBaseSaveChanges);

                if (localStatus.CombineStatuses(transactionSaveChanges).HasErrors)
                {
                    return(localStatus);
                }

                localStatus.SetResult(transactionSaveChanges.Result);

                var duringPostValueTask = _eachEventRunner.RunDuringSaveChangesEventsAsync(context, true, false);

                if (!duringPostValueTask.IsCompleted)
                {
                    throw new InvalidOperationException("Can only run sync tasks");
                }
                if (localStatus.CombineStatuses(duringPostValueTask.Result).HasErrors)
                {
                    return(localStatus);
                }

                transaction?.Commit();

                return(localStatus);
            }

            var status          = new StatusGenericHandler <int>();
            var hasDuringEvents = _eachEventRunner.SetupDuringEvents(context);

            var beforeValueTask = _eachEventRunner.RunBeforeSaveChangesEventsAsync(context, false);

            if (!beforeValueTask.IsCompleted)
            {
                throw new InvalidOperationException("Can only run sync tasks");
            }
            status.CombineStatuses(beforeValueTask.Result);
            if (!status.IsValid)
            {
                return(status);
            }

            context.ChangeTracker.DetectChanges();

            //This runs any actions adding to the config that match this DbContext type
            RunAnyAfterDetectChangesActions(context);

            //Call SaveChanges with catch for exception handler
            IStatusGeneric <int> callSaveChangesStatus;

            if (!hasDuringEvents)
            {
                //No need for a transaction as no During event. Therefore just call SaveChanges
                callSaveChangesStatus = CallSaveChangesWithExceptionHandler(context, callBaseSaveChanges);
            }
            else if (context.Database.CurrentTransaction == null && context.Database.CreateExecutionStrategy().RetriesOnFailure)
            {
                //There is no existing transactions AND we have to handle retries, then we need to wrap the transaction in a retry
                callSaveChangesStatus = context.Database.CreateExecutionStrategy().Execute(RunTransactionWithDuringSaveChangesEvents);
                context.ClearDuringEvents();  //clear During events after a successful transaction
            }
            else
            {
                callSaveChangesStatus = RunTransactionWithDuringSaveChangesEvents();
                context.ClearDuringEvents();  //clear During events after a successful transaction
            }

            if (status.CombineStatuses(callSaveChangesStatus).HasErrors)
            {
                return(status);
            }

            //Copy over the saveChanges result
            status.SetResult(callSaveChangesStatus.Result);

            var afterValueTask = _eachEventRunner.RunAfterSaveChangesEventsAsync(context, false);

            if (!afterValueTask.IsCompleted && !afterValueTask.IsFaulted)
            {
                throw new InvalidOperationException("Can only run sync tasks");
            }
            if (afterValueTask.IsFaulted)
            {
                throw afterValueTask.Result;
            }

            return(status);
        }
Example #20
0
        /// <summary>
        /// This runs the events before, during and after the base SaveChangesAsync method is run
        /// </summary>
        /// <param name="context">The current DbContext</param>
        /// <param name="callBaseSaveChangesAsync">A function that is linked to the base SaveChangesAsync in your DbContext</param>
        /// <param name="cancellationToken"></param>
        /// <returns>Returns the status with the numUpdated number from SaveChanges</returns>
        public async Task <IStatusGeneric <int> > RunEventsBeforeDuringAfterSaveChangesAsync(DbContext context,
                                                                                             Func <Task <int> > callBaseSaveChangesAsync, CancellationToken cancellationToken)
        {
            async Task <IStatusGeneric <int> > RunTransactionWithDuringSaveChangesEventsAsync()
            {
                var localStatus = new StatusGenericHandler <int>();

                //If there is a current transaction then use that, otherwise
                using var transaction = context.Database.CurrentTransaction == null
                    ? await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false)
                    : null;

                var duringPreStatus = await _eachEventRunner.RunDuringSaveChangesEventsAsync(context, false, true)
                                      .ConfigureAwait(false);

                localStatus.CombineStatuses(duringPreStatus);

                var transactionSaveChanges = await CallSaveChangesWithExceptionHandlerAsync(context, callBaseSaveChangesAsync)
                                             .ConfigureAwait(false);

                if (localStatus.CombineStatuses(transactionSaveChanges).HasErrors)
                {
                    return(localStatus);
                }

                localStatus.SetResult(transactionSaveChanges.Result);

                var duringPostStatus = await _eachEventRunner.RunDuringSaveChangesEventsAsync(context, true, true)
                                       .ConfigureAwait(false);

                if (localStatus.CombineStatuses(duringPostStatus).HasErrors)
                {
                    return(localStatus);
                }

                if (transaction != null)
                {
                    await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
                }

                return(localStatus);
            }

            var status          = new StatusGenericHandler <int>();
            var hasDuringEvents = _eachEventRunner.SetupDuringEvents(context);

            status.CombineStatuses(await _eachEventRunner.RunBeforeSaveChangesEventsAsync(context, true).ConfigureAwait(false));
            if (!status.IsValid)
            {
                return(status);
            }

            context.ChangeTracker.DetectChanges();

            //This runs any actions adding to the config that match this DbContext type
            RunAnyAfterDetectChangesActions(context);

            //Call SaveChangesAsync with catch for exception handler
            IStatusGeneric <int> callSaveChangesStatus;

            if (!hasDuringEvents)
            {
                //No need for a transaction as no During event. Therefore just call SaveChanges
                callSaveChangesStatus = await CallSaveChangesWithExceptionHandlerAsync(context, callBaseSaveChangesAsync)
                                        .ConfigureAwait(false);
            }
            else if (context.Database.CurrentTransaction == null && context.Database.CreateExecutionStrategy().RetriesOnFailure)
            {
                //There is no existing transactions AND we have to handle retries, then we need to wrap the transaction in a retry
                callSaveChangesStatus = await context.Database.CreateExecutionStrategy().ExecuteAsync(async x =>
                                                                                                      await RunTransactionWithDuringSaveChangesEventsAsync().ConfigureAwait(false), cancellationToken);

                context.ClearDuringEvents();  //clear During events after a successful transaction
            }
            else
            {
                callSaveChangesStatus = await RunTransactionWithDuringSaveChangesEventsAsync().ConfigureAwait(false);

                context.ClearDuringEvents();  //clear During events after a successful transaction
            }

            if (status.CombineStatuses(callSaveChangesStatus).HasErrors)
            {
                return(status);
            }
            do
            {
                try
                {
                    status.SetResult(await callBaseSaveChangesAsync().ConfigureAwait(false));
                    break; //This breaks out of the do/while
                }
                catch (Exception e)
                {
                    IStatusGeneric exceptionStatus = null;
                    if (_config.ExceptionHandlerDictionary.TryGetValue(context.GetType(), out var exceptionHandler))
                    {
                        exceptionStatus = exceptionHandler(e, context);
                    }
                    if (exceptionStatus == null)
                    {
                        //This means the SaveChangesExceptionHandler doesn't cover this type of Concurrency Exception
                        throw;
                    }
                    //SaveChangesExceptionHandler ran, so combine its error into the outer status
                    status.CombineStatuses(exceptionStatus);
                }
                //If the SaveChangesExceptionHandler fixed the problem then we call SaveChanges again, but with the same exception catching.
            } while (status.IsValid);
            await _eachEventRunner.RunAfterSaveChangesEventsAsync(context, true).ConfigureAwait(false);

            return(status);
        }
Example #21
0
        public IStatusGeneric <int> StatusGenericNum(int i)
        {
            var status = new StatusGenericHandler <int>();

            //series of tests and then return all the errors together
            //Good because the user gets all the errors at once
            if (i == 20)
            {
                status.AddError("input must not be 20", nameof(i));
            }
            if (i == 30)
            {
                status.AddError("input must not be 30", nameof(i));
            }
            if (i == 40)
            {
                status.AddError("input must not be 40", nameof(i));
            }
            if (i == 50)
            {
                status.AddError("input must not be 50", nameof(i));
            }
            if (!status.IsValid)
            {
                return(status);
            }

            //add error and return immediately
            if (i <= 0)
            {
                return(status.AddError("input must be positive", nameof(i)));
            }

            //combine error from another non-StatusGeneric method and return if has errors
            status.CombineStatuses(NonStatusGenericString(i == 10 ? null : "good"));
            if (status.HasErrors)
            {
                return(status);
            }

            //combine errors from another generic status, keeping the status to extract later
            var stringStatus = StatusGenericNumReturnString(i);

            if (status.CombineStatuses(stringStatus).HasErrors)
            {
                return(status);
            }

            //Do something with the Result from the StatusGenericNumReturnString method
            var combinedNums = int.Parse(stringStatus.Result) + i;

            //Set the result to be returned from this method if there are no errors
            status.SetResult(combinedNums);

            //You can put tests just before a result would be returned  - any error will set the result to default value
            if (combinedNums <= 0)
            {
                status.AddError("input must be positive", nameof(i));
            }

            //If you return a IStatusGeneric<T> and there are errors then the result will be set to default
            return(status);
        }
Example #22
0
        public async ValueTask <IStatusGeneric> RunBeforeSaveChangesEventsAsync(DbContext context, bool allowAsync)
        {
            var status = new StatusGenericHandler();

            //This has to run until there are no new events, because one event might trigger another event
            bool shouldRunAgain;
            int  loopCount = 1;

            do
            {
                var eventsToRun = new List <EntityAndEvent>();
                foreach (var entityEntry in context.ChangeTracker.Entries <IEntityWithBeforeSaveEvents>())
                {
                    eventsToRun.AddRange(entityEntry.Entity.GetBeforeSaveEventsThenClear()
                                         .Select(x => new EntityAndEvent(entityEntry.Entity, x)));
                }

                shouldRunAgain = false;
                foreach (var entityAndEvent in eventsToRun)
                {
                    shouldRunAgain = true;
                    if (allowAsync)
                    {
                        status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync(
                                                   entityAndEvent, loopCount, BeforeDuringOrAfter.BeforeSave, allowAsync)
                                               .ConfigureAwait(false));
                    }
                    else
                    {
                        var findRunStatus =
                            _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, loopCount, BeforeDuringOrAfter.BeforeSave, allowAsync);
                        if (!findRunStatus.IsCompleted)
                        {
                            throw new InvalidOperationException("Can only run sync tasks");
                        }
                        status.CombineStatuses(findRunStatus.Result);
                    }
                    if (!status.IsValid && _config.StopOnFirstBeforeHandlerThatHasAnError)
                    {
                        break;
                    }
                }
                if (loopCount++ > _config.MaxTimesToLookForBeforeEvents)
                {
                    throw new GenericEventRunnerException(
                              $"The BeforeSave event loop exceeded the config's {nameof(GenericEventRunnerConfig.MaxTimesToLookForBeforeEvents)}" +
                              $" value of {_config.MaxTimesToLookForBeforeEvents}. This implies a circular sets of events. " +
                              "Look at EventsRunner Information logs for more information on what event handlers were running.",
                              eventsToRun.Last().CallingEntity, eventsToRun.Last().EntityEvent);
                }
            } while (shouldRunAgain && (status.IsValid || !_config.StopOnFirstBeforeHandlerThatHasAnError));

            if (!status.IsValid)
            {
                //If errors then clear any extra before/after events.
                //We need to do this to ensure another call to SaveChanges doesn't get the old events
                context.ChangeTracker.Entries <IEntityWithBeforeSaveEvents>()
                .ToList().ForEach(x => x.Entity.GetBeforeSaveEventsThenClear());
                context.ClearDuringEvents();
                context.ChangeTracker.Entries <IEntityWithAfterSaveEvents>()
                .ToList().ForEach(x => x.Entity.GetAfterSaveEventsThenClear());
            }

            return(status);
        }
        /// <summary>
        /// This runs the events before, during and after the base SaveChangesAsync method is run
        /// </summary>
        /// <param name="context">The current DbContext</param>
        /// <param name="callBaseSaveChangesAsync">A function that is linked to the base SaveChangesAsync in your DbContext</param>
        /// <param name="cancellationToken"></param>
        /// <returns>Returns the status with the numUpdated number from SaveChanges</returns>
        public async Task <IStatusGeneric <int> > RunEventsBeforeDuringAfterSaveChangesAsync(DbContext context,
                                                                                             Func <Task <int> > callBaseSaveChangesAsync, CancellationToken cancellationToken)
        {
            var eachEventRunner = new RunEachTypeOfEvents(_serviceProvider, _logger, _config);

            async Task <IStatusGeneric <int> > RunTransactionWithDuringSaveChangesEventsAsync()
            {
                var localStatus = new StatusGenericHandler <int>();

                //If there is a current transaction then use that, otherwise
                using var transaction = context.Database.CurrentTransaction == null
                    ? await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false)
                    : null;

                var duringPreStatus = await eachEventRunner.RunDuringSaveChangesEventsAsync(false, true)
                                      .ConfigureAwait(false);

                localStatus.CombineStatuses(duringPreStatus);

                var transactionSaveChanges = await CallSaveChangesWithExceptionHandlerAsync(context, callBaseSaveChangesAsync)
                                             .ConfigureAwait(false);

                if (localStatus.CombineStatuses(transactionSaveChanges).HasErrors)
                {
                    return(localStatus);
                }

                localStatus.SetResult(transactionSaveChanges.Result);

                var duringPostStatus = await eachEventRunner.RunDuringSaveChangesEventsAsync(true, true)
                                       .ConfigureAwait(false);

                if (localStatus.CombineStatuses(duringPostStatus).HasErrors)
                {
                    return(localStatus);
                }

                if (transaction != null)
                {
                    await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
                }

                return(localStatus);
            }

            var status = new StatusGenericHandler <int>();

            status.CombineStatuses(await eachEventRunner.RunBeforeSaveChangesEventsAsync(context, true).ConfigureAwait(false));
            if (!status.IsValid)
            {
                return(status);
            }

            var hasDuringEvents = !_config.NotUsingDuringSaveHandlers && eachEventRunner.SetupDuringEvents(context);

            context.ChangeTracker.DetectChanges();

            //This runs any actions adding to the config that match this DbContext type
            RunAnyAfterDetectChangesActions(context);

            //Call SaveChangesAsync with catch for exception handler
            IStatusGeneric <int> callSaveChangesStatus;

            if (!hasDuringEvents)
            {
                //No need for a transaction as no During event. Therefore just call SaveChanges
                callSaveChangesStatus = await CallSaveChangesWithExceptionHandlerAsync(context, callBaseSaveChangesAsync)
                                        .ConfigureAwait(false);
            }
            else if (context.Database.CurrentTransaction == null && context.Database.CreateExecutionStrategy().RetriesOnFailure)
            {
                //There is no existing transactions AND we have to handle retries, then we need to wrap the transaction in a retry
                callSaveChangesStatus = await context.Database.CreateExecutionStrategy().ExecuteAsync(async x =>
                                                                                                      await RunTransactionWithDuringSaveChangesEventsAsync().ConfigureAwait(false), cancellationToken);

                context.ClearDuringEvents();  //clear During events after a successful transaction
            }
            else
            {
                callSaveChangesStatus = await RunTransactionWithDuringSaveChangesEventsAsync().ConfigureAwait(false);

                context.ClearDuringEvents();  //clear During events after a successful transaction
            }

            if (status.CombineStatuses(callSaveChangesStatus).HasErrors)
            {
                return(status);
            }

            //Copy over the saveChanges result
            status.SetResult(callSaveChangesStatus.Result);

            await eachEventRunner.RunAfterSaveChangesEventsAsync(context, true);

            return(status);
        }