samber / do

⚙️ A dependency injection toolkit based on Go 1.18+ Generics.
https://pkg.go.dev/github.com/samber/do
MIT License
1.71k stars 67 forks source link

Proposal: allow service providers registered in the root scope to be invoked with values and other providers in the scoped injector #83

Open chunming-lu opened 2 months ago

chunming-lu commented 2 months ago

Thanks for the framework, which provides perfect experience on dependency injection in my application.

However when I'm using the framework, I find that the providers registered in the root/parent scope will only be invoked in the registered scope. For example the test case blow:

type SomeService struct {
    someRuntimeVal int
}

func ProvideSomeService(injector do.Injector) (*SomeService, error) {
    val := do.MustInvokeNamed[int](injector, "someVal")
    return &SomeService{
        someRuntimeVal: val,
    }, nil
}

func TestRuntimeInvoke(t *testing.T) {
    rootInjector := do.DefaultRootScope
    do.Provide(rootInjector, ProvideSomeService)

    runtimeInjector := rootInjector.Clone().Scope("runtime")
    do.ProvideNamedValue(runtimeInjector, "someVal", 100)
    service, err := do.Invoke[*SomeService](runtimeInjector)
    if err != nil {
        t.Errorf("Failed to invoke service: %v", err)
    } else if service.someRuntimeVal != 100 {
        t.Errorf("Wrong instance initialization: expected: 100, actual: %v", service.someRuntimeVal)
    }
}

I would expect the service provider can be injected in the root scope, and can be invoked correctly in each forked scope, with correct runtime values. In current framework, the test case will fail when invoking *SomeService, with error message:

Failed to invoke service: DI: could not find service someVal, available services: *SomeService, path: *SomeService -> someVal

I read the code and find that when creating scopes, the services are not deep copied to the scoped injector, so the child scopes are using the same service instance in the parent scope. It's the same with Injector.Clone() method since it also only shadow copies the services. It would be best if each injector can have its own provider management namespace and invoke the owning and inheriting providers independently.

samber commented 2 months ago

A service can depend on a service of the same scope or from ancestors.

In your example, *SomeService should be injected in runtimeInjector and someVal in root scope.