uber-go/fx

fx.Decorate does not add a missing object to the module

Closed this issue · 3 comments

Describe the bug

If I add an fx.Decorate to my module that returns a type that doesn't exist in the container otherwise, the value isn't made available to the module.

To Reproduce

  1. Define a type and a constructor for it that returns named values only:
type DBConn struct{ name string }

// Note that a naked DBConn will not be added to the container.
// Only named variants.

func NewDB() (out struct {
	fx.Out
	RW *DBConn `name:"rw"`
	RO *DBConn `name:"ro"`
}) {
	out.RW = &DBConn{name: "rw"}
	out.RO = &DBConn{name: "ro"}
	return
}
  1. Add helper functions that a module can use to pick from one of the named variants.
// UseRW opts the current module to use the rw connection.
func UseRW() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RW *DBConn `name:"rw"`
	}) *DBConn {
		return i.RW
	})
}

// UseRO opts the current module to use the ro connection.
func UseRO() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RO *DBConn `name:"ro"`
	}) *DBConn {
		return i.RO
	})
}
  1. Attempt to use these in main.
func main() {
	fx.New(
		fx.Provide(
			NewDB,
		),
		fx.Module("uses-rw",
			UseRW(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-rw:", conn.name)
			}),
		),
		fx.Module("uses-ro",
			UseRO(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-ro:", conn.name)
			}),
		),
	).Run()
}
Full program
package main

import "go.uber.org/fx"

type DBConn struct{ name string }

// Note that a naked DBConn will not be added to the container.
// Only named variants.

func NewDB() (out struct {
	fx.Out
	RW *DBConn `name:"rw"`
	RO *DBConn `name:"ro"`
}) {
	out.RW = &DBConn{name: "rw"}
	out.RO = &DBConn{name: "ro"}
	return
}

// Helpers that use fx.Decorate to pick one of the named variants
// but only within the current module's scope.

// UseRW opts the current module to use the rw connection.
func UseRW() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RW *DBConn `name:"rw"`
	}) *DBConn {
		return i.RW
	})
}

// UseRO opts the current module to use the ro connection.
func UseRO() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RO *DBConn `name:"ro"`
	}) *DBConn {
		return i.RO
	})
}

func main() {
	fx.New(
		fx.Provide(
			NewDB,
		),
		fx.Module("uses-rw",
			UseRW(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-rw:", conn.name)
			}),
		),
		fx.Module("uses-ro",
			UseRO(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-ro:", conn.name)
			}),
		),
	).Run()
}

Running this fails with the error:

% go run .
[Fx] PROVIDE    *main.DBConn[name = "rw"] <= main.NewDB()
[Fx] PROVIDE    *main.DBConn[name = "ro"] <= main.NewDB()
[Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] DECORATE   *main.DBConn <= main.UseRW.func1() from module "uses-rw"
[Fx] DECORATE   *main.DBConn <= main.UseRO.func1() from module "uses-ro"
[Fx] INVOKE             main.main.func1() from module "uses-rw"
[Fx] ERROR              fx.Invoke(main.main.func1()) called from:
main.main
        [..]/fx-decorate-bug/main.go:53
runtime.main
        [..]/go/1.21.5/libexec/src/runtime/proc.go:267
Failed: missing dependencies for function "main".main.func1
        [..]/fx-decorate-bug/main.go:53:
missing type:
        - *main.DBConn (did you mean to Provide it?)
[Fx] ERROR              Failed to start: missing dependencies for function "main".main.func1
        [..]/fx-decorate-bug/main.go:53:
missing type:
        - *main.DBConn (did you mean to Provide it?)
exit status 1

Expected behavior

The program should run successfully.

Additional context

To get the desired behavior, a placeholder value is needed in the container.
This works, for example:

 func main() {
 	fx.New(
+		fx.Supply(&DBConn{name: "invalid"}),
		fx.Provide(

Library versions:

go.uber.org/fx v1.20.1
go.uber.org/dig v1.17.1

I don't think this is intended behavior, but I could be wrong.

@abhinav this is the intended design for fx.Decorate. There's a test asserting this behavior also: https://github.com/uber-go/fx/blob/master/decorate_test.go#L471

Thinking about it, that probably means we didn't document this behavior properly. Let me fix that.

Oops, you're right @sywhang. This is intended behavior.
I wasn't sure if this would work or not, and since the documentation didn't say otherwise, I assumed it would.
Agree with fixing the documentation for this.