-
Notifications
You must be signed in to change notification settings - Fork 87
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
Comments
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. |
@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! |
Hi @GuillaumeSalles, Yeah – my plan is to use 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. |
@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? |
Ah, good question @cmeeren . The store implementation I'm using to explore this idea (not redux.net) statically types actions, and uses So registering a saga via (I forgot to include the action in the saga function originally.)
|
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. |
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. |
@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. 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 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 store.Actions
.Where(a => a is MyActionType)
.Cast<MyActionType>()
.Throttle(TimeSpan.FromSeconds(1))
.RunsSaga(store, MySaga) where e.g. This approach presents a bit of a challenge for my target model of |
I'll take a look at your code when time permits. For now, here are some immediate thoughts:
|
#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. |
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. |
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>() |
If you're talking about the fact that the original action dispatchers cause no side effects (e.g., a view/VM only dispatches If that's not what you meant, I'm afraid I didn't quite understand.
|
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. |
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. |
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. :) |
@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,
I don't see how these peculiarities transfer to C#. To me, it seems that everything would be attainable just using 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 Footnote: My listeners are 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;
};
}
} |
@dcolthorp, I might have a solution for the problem you describe in #47 (comment). Take a look at PR #62. |
@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 |
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. |
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. |
@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? |
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. |
Hi,
It will be great if we can have such a Redux-Saga implementation.
The text was updated successfully, but these errors were encountered: