/// <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); }
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); }
//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); } } }
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); }
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); }
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 }
//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); }
//------------------------------------------ // 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); }
/// <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); }
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); }
/// <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); }
/// <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); }
/// <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); }
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); }
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); }