Open Rajivhost opened 7 years ago
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 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.
@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 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.
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. 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.
I'll take a look at your code when time permits. For now, here are some immediate thoughts:
I like the idea of an IObservable of actions. (I can't see the full consequences of it right now, though.)
Concerning sagas and multiple actions, am I right in assuming that what you propose is this?
object
), and use switch
or similar inside the saga.If that's the case - I like it!
Given an IObservable of actions, could the reducers simply work by subscribing to store.Actions
?
@cmeeren.
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>()
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!
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,
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;
};
}
}
@dcolthorp, I might have a solution for the problem you describe in https://github.com/GuillaumeSalles/redux.NET/issues/47#issuecomment-290694783. 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 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
.
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.