mrpmorris / Fluxor

Fluxor is a zero boilerplate Flux/Redux library for Microsoft .NET and Blazor.
MIT License
1.24k stars 141 forks source link

Static state reducers inside an action class with a parameterized constructor #292

Closed uhfath closed 2 years ago

uhfath commented 2 years ago

Recently stumbled on a case when I moved a static state reducer inside an action class which has a parametrized constructor. Here is a repro.

Based on a stock sample I've modified a IncrementCounterAction class like this:

public class IncrementCounterAction
{
    public int Step { get; }

    public IncrementCounterAction(int step)
    {
        Step = step;
    }

    [ReducerMethod]
    public static CounterState ReduceIncrementCounterAction(CounterState state, IncrementCounterAction action) =>
        new(clickCount: state.ClickCount + action.Step);
}

Two moments here:

  1. the static reducer method, which previously was a part of a separate static class Reducers;
  2. a constructor which takes an argument to force creation of an action with a parameter.

The issue is that this action class gets registered in a DI container automatically. Perhaps because of a RecuderMethod attribute being used. When launched there is an exception:

Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction Lifetime: Scoped ImplementationType: BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction': Unable to resolve service for type 'System.Int32' while attempting to activate 'BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction'.)
 ---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction Lifetime: Scoped ImplementationType: BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction': Unable to resolve service for type 'System.Int32' while attempting to activate 'BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction'.
 ---> System.InvalidOperationException: Unable to resolve service for type 'System.Int32' while attempting to activate 'BasicConcepts.StateActionsReducersTutorial.Store.IncrementCounterAction'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceDescriptor serviceDescriptor, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
   at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
   at BasicConcepts.StateActionsReducersTutorial.Program.Main(String[] args) in D:\Projects\TestFluxorReducers\TestFluxorReducers\Program.cs:line 16

If I remove the static reducer from the action and put it back to a separate static class than everything works. Is this an intentional behavior?

The reducer is moved to an action class to simplify development process. When clicking an action in IDE it opens the action's code along with it's reducers. Of course one could simply move the reducers in another nested static private class like this:

public class IncrementCounterAction
{
    public int Step { get; }

    public IncrementCounterAction(int step)
    {
        Step = step;
    }

    private static class Reducers
    {
        [ReducerMethod]
        public static CounterState ReduceIncrementCounterAction(CounterState state, IncrementCounterAction action) =>
            new(clickCount: state.ClickCount + action.Step);
    }
}

It's a sort of a feasible workaround, but just a bit more code to write. Just wanted to make sure it's not a bug or something.

P.S. Tested on a 5.2.0

mrpmorris commented 2 years ago

You definitely shouldn't be putting reducer code in actions. Actions are a statement of intent only, and state is only what the client believes is true.

Reducers should be a separate concern, that know what the implications are of the user's intent and changes what you now know to be true.

uhfath commented 2 years ago

I get your point, thanks.