public async Task ExampleUsage2() { var t = Log.MethodEntered("DataStoreExample2.ExampleUsage2"); // Add a thunk middleware to allow dispatching async actions: var thunkMiddleware = Middlewares.NewThunkMiddleware <MyAppState1>(); // aDD A logging middleware to log all dispatched actions: var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>(); // Add a recorder middleware to enable hot reload by replaying previously recorded actions: var recorder = new ReplayRecorder <MyAppState1>(); var recMiddleware = recorder.CreateMiddleware(); var undoable = new UndoRedoReducer <MyAppState1>(); // To allow undo redo on the full store wrap the main reducer with the undo reducer: var undoReducer = undoable.Wrap(MyReducers1.ReduceMyAppState1); var data = new MyAppState1(null, null, 0); // the initial immutable state var store = new DataStore <MyAppState1>(undoReducer, data, loggingMiddleware, recMiddleware, thunkMiddleware); store.storeName = "Store 1"; TestNormalDispatchOfActions(store); TestUndoAndRedo(store); await TestAsyncActions(store); await TestReplayRecorder(recorder, store); TestSkippableUndoActions(store); Log.MethodDone(t); }
public void TestMakeDebugCopyOfAction() { { var actionToClone = new TestAction1("abc"); bool copyOfActionSupported = false; object actionBeforeDispatch = null; Middlewares.MakeDebugCopyOfAction(actionToClone, ref copyOfActionSupported, ref actionBeforeDispatch); Assert.False(copyOfActionSupported); } { var actionToClone = new TestAction2("abc"); bool copyOfActionSupported = false; object actionBeforeDispatch = null; Middlewares.MakeDebugCopyOfAction(actionToClone, ref copyOfActionSupported, ref actionBeforeDispatch); Assert.True(copyOfActionSupported); } { var objectToClone = new TestAction3() { SomeDir = EnvironmentV2.instance.GetNewInMemorySystem() }; bool copyOfActionSupported = false; object actionBeforeDispatch = null; Middlewares.MakeDebugCopyOfAction(objectToClone, ref copyOfActionSupported, ref actionBeforeDispatch); Assert.False(copyOfActionSupported); } }
public async Task <ISaga> Handle(ExecuteStepCommand command) { ISaga saga = command.Saga; ISagaStep step = command.SagaStep; ISagaAction sagaAction = command.SagaAction; ISagaModel model = command.Model; StepData stepData = GetOrCreateStepData(saga, step, model); MiddlewaresChain middlewaresChain = Middlewares.BuildFullChain( serviceProvider, SaveSaga, ExecuteStep); Exception executionError = null; try { await Middlewares.ExecuteChain( middlewaresChain, saga, step, stepData); stepData. SetSucceeded(saga.ExecutionState, dateTimeProvider); } catch (SagaStopException) { throw; return(null); } catch (Exception ex) { logger. LogError(ex, $"Saga: {saga.Data.ID}; Executing {(step.Async ? "async " : "")}step: {step.StepName}"); executionError = ex; stepData. SetFailed(saga.ExecutionState, dateTimeProvider, executionError.ToSagaStepException()); } finally { middlewaresChain. Clean(); stepData. SetEnded(saga.ExecutionState, dateTimeProvider); } string nextStepName = CalculateNextStepName( saga, step, sagaAction, stepData, executionError); SaveNextStep(saga, stepData, nextStepName); CheckIfSagaIsDeleted(saga); await sagaPersistance.Set(saga); return(saga); }
void setupImmutableDatastore() { Log.MethodEntered(); var log = Middlewares.NewMutationBroadcasterMiddleware <MyDataModel>(); var store = new DataStore <MyDataModel>(MainReducer, new MyDataModel(new MyDataModel.SubSection1(string1: "", bool1: false)), log); IoC.inject.SetSingleton <IDataStore <MyDataModel> >(store); }
private static DataStore <MyModel> NewDataStore() { MyModel model = new MyModel(null, ImmutableList <MyCircle> .Empty); Middleware <MyModel> exampleMiddleware = Middlewares.NewLoggingMiddleware <MyModel>(); UndoRedoReducer <MyModel> undoLogic = new UndoRedoReducer <MyModel>(); return(new DataStore <MyModel>(undoLogic.Wrap(MyReducer), model, exampleMiddleware)); }
public static async Task ShowIn(ViewStack viewStack) { MyModel model = new MyModel(null, ImmutableList <MyUser> .Empty); Middleware <MyModel> exampleMiddleware = Middlewares.NewLoggingMiddleware <MyModel>(); UndoRedoReducer <MyModel> undoLogic = new UndoRedoReducer <MyModel>(); DataStore <MyModel> store = new DataStore <MyModel>(undoLogic.Wrap(MyReducer), model, exampleMiddleware); MyPresenter presenter = new MyPresenter(); presenter.targetView = viewStack.ShowView("7GUIs_Task5_CRUD"); await presenter.LoadModelIntoView(store); }
public Middleware <TContext> Build() { foreach (var hook in buildHooks) { hook(); } // Assemble the middlewares sequentially Func <TContext, Task> seed = (ctx) => Task.CompletedTask; var reversedMiddlewares = Middlewares.Reverse(); var handler = reversedMiddlewares.Aggregate(seed, (x, y) => (TContext ctx) => y(ctx, x)); Middleware <TContext> middleware = (ctx) => handler(ctx); return(middleware); }
public override IEnumerator RunTest() { // Create an immutable datastore that will contain the data model in this example: var log = Middlewares.NewLoggingMiddleware <MyDataModel3>(); IDataStore <MyDataModel3> store = new DataStore <MyDataModel3>(MainReducer, new MyDataModel3(), log); IoC.inject.SetSingleton(store); // Create a presenter that connectes the model with the view (the Unity UI): var currentUserPresenter = new MyUserUi3(); // Set the target view by loading it from a prefab and setting the root GO: currentUserPresenter.targetView = ViewStackHelper.MainViewStack().ShowView("MyUserUi1"); // Connect the model changes with the presenter: currentUserPresenter.ListenToStoreUpdates(store, state => state.currentUser); // Dispatch a first setUser action to update the UI: store.Dispatch(new ActionSetNewUser() { newUser = new MyUser3("Carl", 99) }); // Delay needed since the UI update simulates a delay too: yield return(new WaitForSeconds(0.5f)); // Check that the UI was automatically updated: AssertV2.AreEqual("Carl", currentUserPresenter.NameUi().text); AssertV2.AreEqual("99", currentUserPresenter.AgeUi().text); // Simulate that the user changed the model via the UI: store.Dispatch(new ActionUpdateUser() { target = store.GetState().currentUser, newValues = new MyUser3("Paul", 0) }); // Delay needed since the UI update simulates a delay too: yield return(new WaitForSeconds(2f)); // Check that the UI was automatically updated: AssertV2.AreEqual("Paul", currentUserPresenter.NameUi().text); AssertV2.AreEqual("0", currentUserPresenter.AgeUi().text); }
private async Task TestReplayRecorderOnNewStore(ReplayRecorder <MyAppState1> recorder, MyAppState1 finalStateOfFirstStore) { var t = Log.MethodEntered("TestReplayRecorderOnNewStore"); // Connect the recorder to the new store: var recMiddleware = recorder.CreateMiddleware(); var undoable = new UndoRedoReducer <MyAppState1>(); var logging = Middlewares.NewLoggingMiddleware <MyAppState1>(); var data2 = new MyAppState1(null, null, 0); var store2 = new DataStore <MyAppState1>(undoable.Wrap(MyReducers1.ReduceMyAppState1), data2, logging, recMiddleware); store2.storeName = "Store 2"; // Replaying the recorder will now fill the second store with the same actions: await recorder.ReplayStore(); AssertEqualJson(finalStateOfFirstStore, store2.GetState()); Log.MethodDone(t); }
public static async Task ShowIn(ViewStack viewStack) { // Call model unit tests manually before the UI is shown: CellsModelTests.TestFromAndToRowName(); CellsModelTests.TestDataStoreTransitiveChanges(); CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); Middleware <CellsModel> logging = Middlewares.NewLoggingMiddleware <CellsModel>(); UndoRedoReducer <CellsModel> undoLogic = new UndoRedoReducer <CellsModel>(); DataStore <CellsModel> store = new DataStore <CellsModel>(undoLogic.Wrap(CellsReducers.MainReducer), model, logging); MyPresenter presenter = new MyPresenter(); presenter.targetView = viewStack.ShowView("7GUIs_Task7_Cells"); await presenter.LoadModelIntoView(store); await TaskV2.Delay(2000); Toast.Show("Now simulating some table model changes.."); // Simulate changes in the model to check if the UI updates correctly: CellsModelTests.SimulateSomeChangesInModel(store); }
public void Use(Func <TContext, Func <TContext, Task>, Task> middleware) => Middlewares.Add(middleware);
public async Task ExampleUsage1() { var t = Log.MethodEntered("DataStoreExample3.ExampleUsage1"); // Add a thunk middleware to allow dispatching async actions: var thunkMiddleware = Middlewares.NewThunkMiddleware <MyAppState1>(); // aDD A logging middleware to log all dispatched actions: var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>(); var serverOutboxHandler = new ServerOutboxHandler <MyAppState1>(); // To allow undo redo on the full store wrap the main reducer with the undo reducer: var outboxReducer = serverOutboxHandler.Wrap(MyReducers1.ReduceMyAppState1); var initialState = new MyAppState1(); // the initial immutable state var store = new DataStore <MyAppState1>(outboxReducer, initialState, loggingMiddleware, thunkMiddleware); IoC.inject.SetSingleton(store); store.storeName = "Store 3"; { // Do a login which is an async server action that cant be cached optimistically and wont work offline: Func <Task> asyncLoginTask = async() => { await TaskV2.Delay(100); store.Dispatch(new ActionUserLoggedIn() { newLoggedInUser = new MyUser1("*****@*****.**") }); }; await(store.Dispatch(asyncLoginTask) as Task); } { // Change the email a first time: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }; store.Dispatch(a); Assert.Equal(a, store.GetState().serverOutbox.serverActions.First()); Assert.False(store.GetState().user.emailConfirmed); } { // Change the email a second time: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }; store.Dispatch(a); Assert.Equal(a, store.GetState().serverOutbox.serverActions.Last()); } Assert.Equal(2, store.GetState().serverOutbox.serverActions.Count); await store.SyncWithServer(store.GetState().serverOutbox.serverActions.First()); Assert.Single(store.GetState().serverOutbox.serverActions); await store.SyncWithServer(store.GetState().serverOutbox.serverActions.First()); Assert.Empty(store.GetState().serverOutbox.serverActions); Assert.True(store.GetState().user.emailConfirmed); { // Simulate a server task that has a timeout: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**", simulateOneTimeout = true }; store.Dispatch(a); Assert.Single(store.GetState().serverOutbox.serverActions); Assert.False(store.GetState().user.emailConfirmed); await store.SyncWithServer(a); Assert.Empty(store.GetState().serverOutbox.serverActions); Assert.Equal(2, a.sentToServerCounter); Assert.True(store.GetState().user.emailConfirmed); } { // Simulate the server rejecting an email change: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**", simulateError = true }; store.Dispatch(a); await store.SyncWithServer(a); Assert.Empty(store.GetState().serverOutbox.serverActions); Assert.Equal("*****@*****.**", store.GetState().user.email); Assert.True(store.GetState().user.emailConfirmed); } { // Test persisting and restoring the full store and continue with the pending server requests: store.Dispatch(new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }); store.Dispatch(new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }); Assert.Equal(2, store.GetState().serverOutbox.serverActions.Count); Assert.False(store.GetState().user.emailConfirmed); Assert.Equal("*****@*****.**", store.GetState().user.email); // Simulate persisiting the store to disk and back into memory: string persistedStateJson = TypedJsonHelper.NewTypedJsonWriter().Write(store.GetState()); store.Destroy(); // Destroy the old store before loading the state again into an new store var data2 = TypedJsonHelper.NewTypedJsonReader().Read <MyAppState1>(persistedStateJson); var store2 = new DataStore <MyAppState1>(outboxReducer, data2, loggingMiddleware, thunkMiddleware); IoC.inject.SetSingleton(store2, overrideExisting: true); store2.storeName = "Store 3 (2)"; Assert.Equal(2, store2.GetState().serverOutbox.serverActions.Count); Assert.False(store2.GetState().user.emailConfirmed); Assert.Equal("*****@*****.**", store2.GetState().user.email); // Sync the pending server tasks one after another: foreach (var serverAction in store2.GetState().serverOutbox.serverActions) { await store2.SyncWithServer(serverAction); } Assert.True(store2.GetState().user.emailConfirmed); Assert.NotNull(store2.GetState().serverOutbox); Assert.Empty(store2.GetState().serverOutbox.serverActions); } }
public static void TestDataStoreTransitiveChanges() { CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); var store = new DataStore <CellsModel>(CellsReducers.MainReducer, model, Middlewares.NewLoggingMiddleware <CellsModel>()); store.Dispatch(new MyActions.SetCell("A", 1, "1 + 1")); var cells = store.SelectElement(s => s.cells); Cell a1 = cells()[new CellPos("A", 1)]; Assert.Equal(2, a1.value); // B1 will depend on A1: store.Dispatch(new MyActions.SetCell("B", 1, "3 * A1")); Cell b1 = cells()[new CellPos("B", 1)]; Assert.Equal(6, b1.value); // C1 will depend on A1 and B1: store.Dispatch(new MyActions.SetCell("C", 1, "B1 + 3 - A1")); Cell c1 = cells()[new CellPos("C", 1)]; Assert.Equal(7, c1.value); // D1 will depend on C1 (so transitivly also on A1 and B1): store.Dispatch(new MyActions.SetCell("D", 1, "2 * C1")); Cell d1 = cells()[new CellPos("D", 1)]; Assert.Equal(14, d1.value); // Now changing A1 must have affects to B1, C1, D1 as well: store.Dispatch(new MyActions.SetCell("A", 1, "4 + 1")); c1 = cells()[new CellPos("C", 1)]; Assert.Equal(13, c1.value); d1 = cells()[new CellPos("D", 1)]; Assert.Equal(26, d1.value); // Select cell C1: store.Dispatch(new MyActions.SelectCell(c1.pos)); Assert.True(cells()[new CellPos("C", 1)].isSelected); // Select cell D1: store.Dispatch(new MyActions.SelectCell(d1.pos)); Assert.True(cells()[new CellPos("D", 1)].isSelected); Assert.False(cells()[new CellPos("C", 1)].isSelected); store.Dispatch(new MyActions.SetCell("A", 3, "1")); store.Dispatch(new MyActions.SetCell("A", 2, "A3 + 1")); Assert.Throws <MyActions.SetCell.SelfRefException>(() => { store.Dispatch(new MyActions.SetCell("A", 3, "A2 + 1")); }); }
public SmartSqlBuilder AddMiddleware(IMiddleware middleware) { Middlewares.Add(middleware); return(this); }
public EletraMiddlewareService(IMapper mapper, IOptionsMonitor <Middlewares> middlewaresAccessor) { _mapper = mapper; _middlewares = middlewaresAccessor.CurrentValue; Manufacturer = "Eletra"; }
/// <summary> /// 获取指定类型的全部中间件 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public IEnumerable <T> GetMiddlewares <T>() { return(Middlewares.Where(m => m is T).Cast <T>()); }
public GrpcServerBuilder AddMiddleware <T>() where T : GrpcMiddlewareBase { Middlewares.Add(typeof(T)); return(this); }
public IApplicationBuilder Use(Func <RequestDelegate, RequestDelegate> middleware) { Middlewares.Add(middleware); return(this); }
private IAction InvokeDispathAction(IAction action) { action = Middlewares.Invoke(action); return(InnerDispath(action)); }
public static async Task TestLoggingOverhead() { StopwatchV2 t1, t2; { t1 = Log.MethodEntered("SimulateManyChangesInModel without logging"); CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); var store = new DataStore <CellsModel>(CellsReducers.MainReducer, model); await SimulateManyChangesInModel(store); Log.MethodDone(t1); } { t2 = Log.MethodEntered("SimulateManyChangesInModel without logging"); CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); var store = new DataStore <CellsModel>(CellsReducers.MainReducer, model, Middlewares.NewLoggingMiddleware <CellsModel>()); await SimulateManyChangesInModel(store); Log.MethodDone(t2); } // Logging makes mutating the model at least double as slow: Assert.True(t1.ElapsedMilliseconds * 2 < t2.ElapsedMilliseconds, $"t1={t1}, t2={t2}"); }
public WebSocketService(IServiceProvider serviceProvider, IOptions <ServerOptions> serverOptions, IChannelCreatorFactory channelCreatorFactory) : base(serviceProvider, serverOptions, channelCreatorFactory) { _sessionContainerMiddleware = Middlewares.FirstOrDefault(m => m is IAsyncSessionContainer || m is ISessionContainer); }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseSwagger(); app.UseSwaggerUI(c => { var swaggerJsonBasePath = string.IsNullOrWhiteSpace(c.RoutePrefix) ? "." : ".."; c.SwaggerEndpoint($"{swaggerJsonBasePath}/swagger/v1.0/swagger.json", Configuration.GetValue <string>("SystemInfo:Name")); }); var basePath = Path.Combine(OptionsClient.GetData(Configuration.GetValue <string>("FileStore:BasePath"))); if (!Directory.Exists(basePath)) { Directory.CreateDirectory(basePath); } app.UseStaticFiles(new StaticFileOptions() { FileProvider = new PhysicalFileProvider(basePath), HttpsCompression = HttpsCompressionMode.Compress }); app.UseCors(builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader()); app.UseResponseCompression(); var headersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor, RequireHeaderSymmetry = false, ForwardLimit = null }; headersOptions.KnownNetworks.Clear(); headersOptions.KnownProxies.Clear(); app.UseForwardedHeaders(headersOptions); app.UseMetricsAllMiddleware(); app.Use(async(context, next) => { await next.Invoke(); Middlewares.AutoDiscoverRoutes(context); }); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapHealthChecks("/health"); endpoints.MapControllers(); }); }
public override PipelineResult <ClientActionContext> Build() { return(new ClientPipelineResult(BuildActionDelegate(), Middlewares.ToList())); }
public void ExampleUsage1() { var t = Log.MethodEntered("DataStoreExample3.ExampleUsage1"); // A middleware that will allow to use mutable data in the data store: var mutableMiddleware = Middlewares.NewMutableDataSupport <MyAppState1>(); // Add A logging middleware to log all dispatched actions: var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>(); MyUser1 user = new MyUser1() { name = "Carl" }; var model = new MyAppState1() { user = user }; var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, model, loggingMiddleware, mutableMiddleware); IoC.inject.SetSingleton(store); store.storeName = "Store 4"; // Setup 3 listeners that react when the name of the user or his contacts change: var userChangedCounter = 0; var userNameChangedCounter = 0; var contact1NameChangedCounter = 0; var contact2NameChangedCounter = 0; store.AddStateChangeListener(s => s.user, (MyUser1 theChangedUser) => { userChangedCounter++; }); store.AddStateChangeListener(s => s.user?.name, (string theChangedName) => { userNameChangedCounter++; }); store.AddStateChangeListener(s => s.user?.contacts?.FirstOrDefault()?.contactData.name, (_) => { contact1NameChangedCounter++; }); store.AddStateChangeListener(s => s.user?.contacts?.Skip(1).FirstOrDefault()?.contactData.name, (_) => { contact2NameChangedCounter++; }); var contact1 = new MyUser1() { name = "Tom" }; { // Add a first contact to the user: Assert.Null(user.contacts); store.Dispatch(new ActionAddContact() { targetUserId = user.id, newContact = new MyContact1() { contactData = contact1 } }); Assert.Same(contact1, user.contacts.First().contactData); Assert.True(user.WasModifiedInLastDispatch()); // Now that there is a contact 1 the listener was triggered: Assert.Equal(1, userChangedCounter); Assert.Equal(0, userNameChangedCounter); Assert.Equal(1, contact1NameChangedCounter); Assert.Equal(0, contact2NameChangedCounter); } var contact2 = new MyUser1() { name = "Bill" }; { // Add a second contact to the user which should not affect contact 1: store.Dispatch(new ActionAddContact() { targetUserId = user.id, newContact = new MyContact1() { contactData = contact2 } }); Assert.Same(contact2, user.contacts.Last().contactData); Assert.True(user.WasModifiedInLastDispatch()); Assert.False(contact1.WasModifiedInLastDispatch()); Assert.Equal(2, userChangedCounter); Assert.Equal(0, userNameChangedCounter); Assert.Equal(1, contact1NameChangedCounter); Assert.Equal(1, contact2NameChangedCounter); } { // Change the name of contact 1 which should not affect contact 2: var newName1 = "Toooom"; store.Dispatch(new ActionChangeUserName() { targetUserId = contact1.id, newName = newName1 }); Assert.True(user.WasModifiedInLastDispatch()); Assert.True(contact1.WasModifiedInLastDispatch()); Assert.False(contact2.WasModifiedInLastDispatch()); Assert.Equal(3, userChangedCounter); Assert.Equal(0, userNameChangedCounter); Assert.Equal(2, contact1NameChangedCounter); Assert.Equal(1, contact2NameChangedCounter); } { // Change the name of the user which should not affect the 2 contacts: var newName = "Caaaarl"; Assert.NotEqual(newName, user.name); Assert.Equal(user.name, store.GetState().user.name); var tBeforeDispatch = user.LastMutation; store.Dispatch(new ActionChangeUserName() { targetUserId = user.id, newName = newName }); Assert.Equal(newName, store.GetState().user.name); Assert.Equal(newName, user.name); Assert.Same(model, store.GetState()); Assert.NotEqual(tBeforeDispatch, user.LastMutation); Assert.True(user.WasModifiedInLastDispatch()); Assert.False(contact1.WasModifiedInLastDispatch()); Assert.False(contact2.WasModifiedInLastDispatch()); Assert.Equal(4, userChangedCounter); Assert.Equal(1, userNameChangedCounter); Assert.Equal(2, contact1NameChangedCounter); Assert.Equal(1, contact2NameChangedCounter); } { // Marking an object mutated while not dispatching will throw an exception: Assert.Throws <InvalidOperationException>(() => { user.name = "Cooorl"; user.MarkMutated(); }); Assert.Equal(4, userChangedCounter); // Count should not have changed Assert.Equal(1, userNameChangedCounter); } }
public async Task RunTestTask() { MyUser1 initialState = null; // Initially no user is logged in var store = new DataStore <MyUser1>(MyReducers1.ReduceUser, initialState, Middlewares.NewLoggingMiddleware <MyUser1>()); var links = gameObject.GetLinkMap(); var loginButton = links.Get <Button>("LoginBtn"); var loginButtonWasClicked = loginButton.SetOnClickAction(async delegate { loginButton.gameObject.Destroy(); await TaskV2.Run(async() => { await TaskV2.Delay(1000); store.Dispatch(new ActionLoginUser() { newLoggedInUser = new MyUser1("Karl") }); }).LogOnError(); Assert.IsNotNull(store.GetState()); }); // Register a listener that is attached to the UI button to demonstrate that its no longer triggered once the button is destroyed: var userNameChangedCounter = 0; var subStateListener = store.NewSubStateListenerForUnity(loginButton, user => user); subStateListener.AddStateChangeListener(x => x?.name, newName => { userNameChangedCounter++; Toast.Show("User name changed to " + newName); Assert.IsFalse(loginButton.IsDestroyed()); }, triggerInstantToInit: false); var userInfoText1 = links.Get <InputField>("UserNameInput1"); ConnectInputFieldUiToModel(userInfoText1, store); var userInfoText2 = links.Get <InputField>("UserNameInput2"); ConnectInputFieldUiToModel(userInfoText2, store); var oldCounterValue = userNameChangedCounter; SimulateButtonClickOn("LoginBtn"); await loginButtonWasClicked; Assert.IsTrue(loginButton.IsDestroyed()); // Since the button was destroyed, the counter should not change anymore: Assert.AreEqual(oldCounterValue, userNameChangedCounter); Toast.Show("Changing user name from background thread..."); await Task.Delay(2000); // When NewSubStateListener instead of NewSubStateListenerForUnity is used, the // event will arrive on the thread where it was dispatched: var wasCalledOnMainThread = true; store.NewSubStateListener(user => user).AddStateChangeListener(x => x.name, newName => { wasCalledOnMainThread = MainThread.isMainThread; }, triggerInstantToInit: false); await TaskV2.Run(async() => { store.Dispatch(new ChangeName() { newName = "Caaarl" }); }); Assert.AreEqual("Caaarl", store.GetState().name); Assert.IsFalse(wasCalledOnMainThread); }