Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saga #47

Open
Rajivhost opened this issue Mar 15, 2017 · 23 comments
Open

Saga #47

Rajivhost opened this issue Mar 15, 2017 · 23 comments

Comments

@Rajivhost
Copy link

Hi,
It will be great if we can have such a Redux-Saga implementation.

@GuillaumeSalles
Copy link
Owner

Hi @Rajivhost,

I tried to implement it in the past but the yield operator is more limited in C# than the javascript operator. The interface would not be as clean.

Feel free to give it a try! A redux-saga implementation in .NET would definitely be impressive.

@GuillaumeSalles
Copy link
Owner

@dcolthorp Did you successfully port the saga concept to C#? If yes, I'm interested in what the public api could be.

As I said earlier, yield C# operator is just an iterator. The javascript yield operator is a generator. I don't think the C# public API could be as clean as the javascript version.

IMO, there is a lot of similarity between Saga and free-monad + interpreter. I never used monad in C# but I can imagine that the syntax can become really heavy.

Crazy idea: don't know if it's possible but use Generalized async return types from C# 7 to build a saga with async await would be rad!

@dcolthorp
Copy link

dcolthorp commented Mar 30, 2017

Hi @GuillaumeSalles,

Yeah – my plan is to use async/await instead of yield, and building toward the power of sagas via System.Threading.Tasks and Rx capabilities, as opposed to trying to directly port sagas and it's generator-based model directly.

Here are a few code sketches. These are meant to be illustrative and not necessarily directly applicable to redux.net.

Perhaps you attach a saga to an event type with something like

public void RegisterSaga<TEvent>(Func<Store<TState>, Task> saga);

A "saga" would just be a function that's passed the store and can dispatch events back into it over time.

public async Task DoSomething(Store<TState> store, StartSomethingAction action)
{
  // Interact with asynchronous services
  await _webClient.Post(/*...*/);
  // Dispatch new actions to e.g. update the UI
  store.Dispatch(new EventOfSomeType());
  // Wait for something to happen in the store.
  await store.Select(s => s.SomeProperty == "someValue").FirstAsync();
}

I'd personally like to be able to await a saga and any sagas that fork off of it. For example, perhaps being able to

await store.Dispatch(new StartSomethingAction());

to be able to wait for all asynchrony caused by the action dispatch to settle before continuing to the next step in an integration test.

That's my thinking for how you might be able to have a rather natural sagas model in C#, at least.

@cmeeren
Copy link

cmeeren commented Mar 30, 2017

@dcolthorp, am I right in assuming that sagas as sketched by you above can only react to state changes, not actions? How does this work, exactly? My listeners are served all actions, and react to them, e.g. SignInRequestedAction will make the listener call the sign-in API and dispatch an appropriate action depending on the result. How would this work of the sagas never see actions?

@dcolthorp
Copy link

dcolthorp commented Mar 30, 2017

Ah, good question @cmeeren . The store implementation I'm using to explore this idea (not redux.net) statically types actions, and uses action.GetType() for dispatch. Reducers and sagas in this formulation can be thought of being invoked after the test on the action type. I don't think that detail is necessarily crucial for the overall approach and applicability to redux.net, but does make the example above a lot more clear.

So registering a saga via RegisterSaga<SomeAction>(mySaga) would invoke mySaga only when an action of type SomeAction is dispatched, and the mySaga function would be passed an argument of type SomeAction that it can use without casting.

(I forgot to include the action in the saga function originally.)

RegisterSaga in this formulation would be similar to takeEvery in redux-saga. takeLatest is trickier – perhaps using a CancellationToken? I figured I'd start with something that gets in the ballpark with with takeEvery semantics and figure out where to go from there.

@cmeeren
Copy link

cmeeren commented Mar 30, 2017

What about sagas wanting multiple actions? I have a listener that displays error messages, and it listens to all XxFailed actions from my API listeners, as well as a few more. And I have a listener that displays dialogs that the user can confirm or reject, and that also listens to a couple of actions.

@dcolthorp
Copy link

To be clear – I did not mean to suggest that type-based dispatch should be adopted for this purpose in redux.net. My intent was merely to share an example of what a task-based saga might look like, using some working examples from my codebase. Including that example muddled the issue. My frame of mind right now is on proving out the idea in my own playground and it didn't transfer well. :-)

Regarding how I'd address that issue with my proof-of-concept Store, I'd define the function to take a base type or interface, and register it with each concrete type:

RegisterSaga<LoginFailed>(errorSaga)
RegisterSaga<FetchRecordsFailed>(errorSaga)

There's also no reason I couldn't add support for generic handlers that are invoked for every action, like classic redux, it just wasn't a capability I really felt was crucial so far in the mobile app I'm building.

@dcolthorp
Copy link

dcolthorp commented Mar 31, 2017

@cmeeren and @GuillaumeSalles

I put together a rough proof of concept of the basic model last night. Here are the unit tests that illustrate the approach more fully. Hopefully this complete example better illustrates the API of my proof-of-concept store. RegisterHandler is akin to adding a reducer to a combineReducers call, but prefiltered to the relevant action type. RegisterSaga does the same – invokes a saga for actions matching the type argument.

It's worth noting that these unit tests simulate running in a separate thread. The saga executes on a different thread from the test, but they proceed in lockstep due to the DispatchAndAwaitAsync semantics.

Regarding our discussion of subscription of sagas, I think using Rx with an IObservable of actions is probably the most powerful and natural approach in C#. It'd be easy enough to simply execute a saga when an IObservable yields an action – e.g. using a combination of Select and Cast on a raw IObservable<SomeActionBaseType>, and you could use e.g. Throttle to control how often a saga is executed. It might look something like:

store.Actions
  .Where(a => a is MyActionType)
  .Cast<MyActionType>()
  .Throttle(TimeSpan.FromSeconds(1))
  .RunsSaga(store, MySaga)

where e.g. RunsSaga sets up the saga to run when the IObservable yields a value.

This approach presents a bit of a challenge for my target model of DispatchAndAwaitAsync being able to wait for all triggered sagas to run (as demonstrated in the test file above), but would be easy and natural without that constraint.

@cmeeren
Copy link

cmeeren commented Mar 31, 2017

I'll take a look at your code when time permits. For now, here are some immediate thoughts:

  1. I like the idea of an IObservable of actions. (I can't see the full consequences of it right now, though.)

  2. Concerning sagas and multiple actions, am I right in assuming that what you propose is this?

    • If a saga wants a single action, select and cast to that, and define your saga signature to only accept that type.
    • If a saga wants multiple actions, subscribe and cast to some base action type (or simply object), and use switch or similar inside the saga.

    If that's the case - I like it!

  3. Given an IObservable of actions, could the reducers simply work by subscribing to store.Actions?

@dcolthorp
Copy link

@cmeeren.

#2 is exactly right.

Re #3 – yes, you could do something similar for reducers, and this would be a natural and powerful middleware in C#. It certainly seems to me that if you had the ability to use Rx to control the invocation of sagas, it'd make sense to allow the same sort of thing for reducers. Doing so would obviate the need for e.g. a throttling middleware.

@cmeeren
Copy link

cmeeren commented Mar 31, 2017

Nice. Again I find myself just leaning more and more towards basing this on Rx. I've tried to get into it the last couple of days and I like it better and better.

@GuillaumeSalles
Copy link
Owner

You only talk about the actions subscription aspect. of redux saga. You don't find the saga "purity" interesting? Personally, I find redux-saga interesting because it let you dispatch actions only with pure functions.

Side note on Rx :

store.Actions
  .Where(a => a is MyActionType)
  .Cast<MyActionType>()

is equivalent to

store.Actions
  .OfType<MyActionType>()

@cmeeren
Copy link

cmeeren commented Apr 1, 2017

Personally, I find redux-saga interesting because it let you dispatch actions only with pure functions.

If you're talking about the fact that the original action dispatchers cause no side effects (e.g., a view/VM only dispatches SignInRequested and lets the saga handle sign-in and dispatching further actions, instead of the async calls being performed by the view/VM), or the fact that all actions themselves are simple "record types", then I completely agree, and that is in fact why I have opted for the similar listener pattern in my own code.

If that's not what you meant, I'm afraid I didn't quite understand.

OfType() is brilliant, by the way!

@GuillaumeSalles
Copy link
Owner

I'm talking about declarative effects. More info :

If you remove this part of redux-saga, I think it become an advance version of your middleware listener.

@GuillaumeSalles
Copy link
Owner

That's why I talked about the limitation of the yield keyword and free-monad.

Redux-saga is a little bit like free-monad but it's still not enough to make saga testable like in javascript. Interesting comments here.

@cmeeren
Copy link

cmeeren commented Apr 1, 2017

Thanks for the clear-up. I didn't really understand anyway, because I have no knowledge of "declarative effects" and "generators" and "monads" (at least not by those names). But we can leave it there for now. :)

@cmeeren
Copy link

cmeeren commented Apr 2, 2017

@GuillaumeSalles I don't really get the relevance of the generator/declarative effect stuff. I read through your links, Beginner Tutorial and Declarative Effects, and it seems that all they seek to accomplish is to make sagas testable. For example,

This separation between Effect creation and Effect execution makes it possible to test our Generator in a surprisingly easy way

I don't see how these peculiarities transfer to C#. To me, it seems that everything would be attainable just using async and await. For example, here's my SignInListener:

public class SignInListener
{
    private readonly IWebApi api;

    public SignInListener(IWebApi api)
    {
        this.api = api;
    }

    public async void Listener(IAction action, RootState state, Dispatcher dispatch)
    {
        if (!(action is SignInRequested actn)) return;

        SignInResponse response = await this.api.SignInAsync(actn.Username, actn.Password);
        if (response.CompletedSuccessfully)
        {
            dispatch(new SignInSuccessful(response.Token, actn.Username));
        }
        else
        {
            dispatch(new SignInFailed(response.ErrorMessage));
        }
    }
}

I register it to my listener middleware as a normal event handler:

var listenerMiddleware = new ListenerMiddleware<RootState>();
listenerMiddleware.ActionReceived += signInListener.Listener;

The Listener method is basically as easy to test as any other method, injecting mocks as dependencies and verifying that they've been called correctly and that mocked return values are processed correctly by the subject under test. .NET devs have long been solidly testing methods using DI and mocks for ages.

Footnote: My listeners are async void since they are event handlers, and are easily testable using AsyncEx.Context. The "act" part of my tests are simply wrapped in AsyncContext.Run(() => ...) and everything's fine. If that had bothered me, I could make the listeners async Task instead (though that would feel wrong considering not all listeners perform async actions).


For the record, here's my listener middleware definition:

public delegate void Listener<in TState>(
    [CanBeNull] IAction action,
    TState state,
    Dispatcher dispatch);

public class ListenerMiddleware<TState>
{
    public event Listener<TState> ActionReceived;

    public Func<Dispatcher, Dispatcher> CreateMiddleware(IStore<TState> store)
    {
        return next => action =>
        {
            TState preActionState = store.GetState();
            IAction ret = next(action);
            this.ActionReceived?.Invoke(action, preActionState, store.Dispatch);
            return ret;
        };
    }
}

@cmeeren
Copy link

cmeeren commented Apr 3, 2017

@dcolthorp, I might have a solution for the problem you describe in #47 (comment). Take a look at PR #62.

@dcolthorp
Copy link

@cmeeren – Excellent! That looks great! I think you're right that it would support the model I'm aiming for.

From what I can tell, your model would support everything I aim to do. Your proof-of-concept implementation isn't exception safe – a saga which throws will cause RemoveOperation to not get invoked, and the exception wouldn't be re-thrown by DispatchAsync, but I don't see any reason that behavior wouldn't be easy to support with a few changes to the AwaitableStore and RunsAsyncSaga.

@cmeeren
Copy link

cmeeren commented Apr 3, 2017

Thanks! I'll have a look at that. I suggest we take further discussion on the actual PR, and see if we can't slowly shape it up into something that can be included in some form or another.

@ahmad2smile
Copy link

I'm coming from React Native to Xamarin and bit new to .Net . I see in the current version store accepts a middleware[] but its not documented. If you @cmeeren can provide a basic example of setting up the a saga with this middleware for a newbie that be much appreciated.

@cmeeren
Copy link

cmeeren commented Jun 3, 2018

@ahmad2smile I haven't used Redux.NET in at least a year (I'm rolling my own simple Redux store in F#). Perhaps someone else can help you?

@ahmad2smile
Copy link

Ok, I'm trying with reading bit of a source code and some example of middlewares you provided here. Maybe I'll just get it done. Thanks anyways for you response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants