Decorate does not work if inside fx.Module and Populate is called from outside Module
CTrando opened this issue · 6 comments
Describe the bug
First, we can:
- create an
fx.Module
- provide a type
A
inside the module - decorate the type
A
inside the module - outside the
fx.Module
, populate typeA
The result is that the decorate function would not have been applied.
To Reproduce
I have a testcase to reproduce this:
func TestReproPopulate(t *testing.T) {
t.Parallel()
var something string
app := fxtest.New(
t,
fx.Module("something",
fx.Provide(func() string {
return "something"
}),
fx.Decorate(func(h string) string {
return "else"
}),
),
fx.Populate(&something),
)
app.RequireStart()
app.RequireStop()
require.Equal(t, "else", something)
}
I expect that the decorate statement would have run to replace the string to else
, but it stays as something
. As a result, this testcase fails. I'm on the latest version of fx
(v1.22.1).
However, this passes:
func TestReproPopulate(t *testing.T) {
t.Parallel()
var something string
app := fxtest.New(
t,
fx.Module("something",
fx.Provide(func() string {
return "something"
}),
),
fx.Decorate(func(h string) string {
return "else"
}),
fx.Populate(&something),
)
app.RequireStart()
app.RequireStop()
require.Equal(t, "else", something)
}
Expected behavior
I expect that the decorate inside the module still runs, and we populate the type after the decorate executes. The testcase above should pass.
Additional context
I believe this is because fx.Populate
runs as an fx.Invoke
, which runs at the top level scope and doesn't look at decorators in the child scope.
Hey @CTrando, fx.Decorate are scoped to the deepest Fx module inside which the decorator was provided. In case 2, that's the root scope which is why the decoration works.
Hey @CTrando, fx.Decorate are scoped to the deepest Fx module inside which the decorator was provided. In case 2, that's the root scope which is why the decoration works.
In the first case, shouldn't the decorator apply to the provide statement because they're in the same module?
My expectation would be that a module provides a given type, and any decorators inside that module would run as normal if necessary to provide that type, is my understanding incorrect?
In the example above, fx.Populate is in the root scope not the scope of the Module where fx.Decorate is called. Moving fx.Populate into the Module where fx.Decorate is called should pass the test.
In the example above, fx.Populate is in the root scope not the scope of the Module where fx.Decorate is called. Moving fx.Populate into the Module where fx.Decorate is called should pass the test.
You're right, the test does pass! So if I had a module that exposed a type, where that type was provided and decorated inside the module, how would I go about viewing that type from outside the module?
Even if I did something like:
app := fxtest.New(
t,
fx.Module("something",
// expose a string type with the annotation myString
fx.Provide(
fx.Annotate(func() string {
return "something"
}, fx.ResultTags(`name:"myString"`)),
),
// use a decorator to adjust the string
fx.Decorate(
fx.Annotate(func(h string) string {
return "else"
}, fx.ParamTags(`name:"myString"`)),
),
),
// try to pull out the type exposed in the module
fx.Provide(
fx.Annotate(func(x string) string {
// check if the decorator ran - but it fails
require.Equal(t, "else", x)
return ""
}, fx.ParamTags(`name:"myString"`)),
),
fx.Populate(&something),
)
This fails too - the decorator changes don't appear outside the module. From what I'm seeing, it means that decorators on types are local to a given module, and their changes don't appear to any caller outside the module that references those types. Is that intentional?
Hey @CTrando - yes, this is intentional and expected behavior. Take a look at https://pkg.go.dev/go.uber.org/fx#hdr-Decorator_scope. I think the idea is that a decorator in some small nested subscope shouldn't be able to just completely hijack some value for the entire application.
That said, is there something preventing you from using your decorator at a higher-level scoping such that it applies everywhere you want it to?
Ah, I see, in that documentation it doesn't matter where the logger
is provided. My mistake then, sorry about that.
That said, is there something preventing you from using your decorator at a higher-level scoping such that it applies everywhere you want it to?
There is nothing preventing me from using decorators at a higher scope, and that's what I ended up doing, I was just curious about this. I think I was just confused at why decorators seemed special here, I viewed them as a piece of the module to be executed. For example:
I think the idea is that a decorator in some small nested subscope shouldn't be able to just completely hijack some value for the entire application.
This makes sense to me, if there's a decorator in a module that affects a type defined outside the module. But for decorators on types inside the module, I find it reasonable that decorators would still run, and that the type is not "complete" until those decorators have run.
Perhaps my understanding of modules is incorrect - from these docs,
An Fx module is a shareable Go library or package that provides self-contained functionality to an Fx application.
I was considering the decorator to be part of that self-contained functionality, it's still a bit surprising to me that its side effects are not considered outside the module.
I agree that this seems intentional, although I wonder if this aligns with the idea of an fx.Module
. You can close this issue, but I'd like to hear your thoughts on this. Thank you for the help!