Skip to content

Impossible to inject struct that is part of a group by itself #1181

@giovannizotta

Description

@giovannizotta

Describe the bug
When trying to inject a struct that is part of a group (tagged via ResultTags) singularily, fx cannot find the requested dependency. Let's say we have two interfaces: GameFetcher and UserFetcher, both embedding the Fetcher interface.

Both implementations are provided as part of the fetchers group, annotated with both interfaces:

func AsFetcher(f any, fetcherType any) any {
	return fx.Annotate(
		f,
		fx.As(new(Fetcher)),
		fx.As(fetcherType),
		fx.ResultTags(`group:"fetchers"`),
	)
}

...

fx.Provide(
	AsFetcher(newGameFetcher, new(GameFetcher)),
	AsFetcher(newUserFetcher, new(UserFetcher)),
)

I am not sure whether this is a bug or whether I'm doing things wrong, but I'd appreciate some guidance.

To Reproduce
https://gist.github.com/giovannizotta/d74a5ccfa28208ad2f582590d10b605c

package main

import (
	"go.uber.org/fx"
)

type Fetcher interface{}

type GameFetcher interface {
	Fetcher
}

type UserFetcher interface {
	Fetcher
}

type GameFetcherImpl struct{}

type UserFetcherImpl struct{}

func newUserFetcher() *UserFetcherImpl {
	return &UserFetcherImpl{}
}

func newGameFetcher() *GameFetcherImpl {
	return &GameFetcherImpl{}
}

type StructUsingAllFetchers struct {
	fetchers []Fetcher
}

func NewStructUsingAllFetchers(fetchers []Fetcher) *StructUsingAllFetchers {
	return &StructUsingAllFetchers{
		fetchers: fetchers,
	}
}

func AsFetcher(f any, fetcherType any) any {
	return fx.Annotate(
		f,
		fx.As(new(Fetcher)),
		fx.As(fetcherType),
		fx.ResultTags(`group:"fetchers"`),
	)
}

func main() {
	fx.New(
		fx.Provide(
			AsFetcher(newGameFetcher, new(GameFetcher)),
			AsFetcher(newUserFetcher, new(UserFetcher)),
			fx.Annotate(
				NewStructUsingAllFetchers,
				fx.ParamTags(`group:"fetchers"`),
			),
		),
		fx.Invoke(func(s *StructUsingAllFetchers) {}),
		fx.Invoke(func(gameFetcher GameFetcher) {}),
	).Run()
}

Output:

[Fx] PROVIDE	main.Fetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] PROVIDE	main.GameFetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] PROVIDE	main.Fetcher[group = "fetchers"] <= fx.Annotate(main.newUserFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.UserFetcher]])
[Fx] PROVIDE	main.UserFetcher[group = "fetchers"] <= fx.Annotate(main.newUserFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.UserFetcher]])
[Fx] PROVIDE	*main.StructUsingAllFetchers <= fx.Annotate(main.NewStructUsingAllFetchers(), fx.ParamTags(["group:\"fetchers\""])
[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] INVOKE		main.main.func1()
[Fx] RUN	provide: fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] RUN	provide: fx.Annotate(main.newUserFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.UserFetcher]])
[Fx] RUN	provide: fx.Annotate(main.NewStructUsingAllFetchers(), fx.ParamTags(["group:\"fetchers\""])
[Fx] INVOKE		main.main.func2()
[Fx] ERROR		fx.Invoke(main.main.func2()) called from:
main.main
	main.go:59
runtime.main
	/opt/homebrew/Cellar/go/1.21.5/libexec/src/runtime/proc.go:267
Failed: missing dependencies for function "main".main.func2
	main.go:59:
missing type:
	- main.GameFetcher (did you mean to use one of *main.StructUsingAllFetchers, fx.DotGraph, fx.Lifecycle, or fx.Shutdowner?)
[Fx] ERROR		Failed to start: missing dependencies for function "main".main.func2
	main.go:59:
missing type:
	- main.GameFetcher (did you mean to use one of *main.StructUsingAllFetchers, fx.DotGraph, fx.Lifecycle, or fx.Shutdowner?)
exit status 1

Expected behavior
I would expect the second invoked function to be injected with the GameFetcher, since fx provided it earlier:

[Fx] PROVIDE	main.GameFetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])

Additional context
I found it possible to achieve what I describe in another way, by injecting a slice of the specific elements []GameFetcher:

package main

import (
	"go.uber.org/fx"
)

type Fetcher interface {
	Fetch()
}

type GameFetcher interface {
	Fetcher
}

type UserFetcher interface {
	Fetcher
}

type GameFetcherImpl struct{}

type UserFetcherImpl struct{}

func (g *GameFetcherImpl) Fetch() {}
func (u *UserFetcherImpl) Fetch() {}

func newUserFetcher() *UserFetcherImpl {
	return &UserFetcherImpl{}
}

func newGameFetcher() *GameFetcherImpl {
	return &GameFetcherImpl{}
}

type StructUsingAllFetchers struct {
	fetchers []Fetcher
}

func NewStructUsingAllFetchers(fetchers []Fetcher) *StructUsingAllFetchers {
	return &StructUsingAllFetchers{
		fetchers: fetchers,
	}
}

type StructUsingSpecificFetcher struct {
	gameFetcher GameFetcher
}

func NewStructUsingSpecificFetcher(f []GameFetcher) *StructUsingSpecificFetcher {
	if len(f) != 1 {
		panic("expected 1 fetcher")
	}
	return &StructUsingSpecificFetcher{
		gameFetcher: f[0],
	}
}

func AsFetcher(f any, fetcherType any) any {
	return fx.Annotate(
		f,
		fx.As(new(Fetcher)),
		fx.As(fetcherType),
		fx.ResultTags(`group:"fetchers"`),
	)
}

func main() {
	fx.New(
		fx.Provide(
			AsFetcher(newGameFetcher, new(GameFetcher)),
			AsFetcher(newUserFetcher, new(UserFetcher)),
			fx.Annotate(
				NewStructUsingAllFetchers,
				fx.ParamTags(`group:"fetchers"`),
			),
			fx.Annotate(
				NewStructUsingSpecificFetcher,
				fx.ParamTags(`group:"fetchers"`),
			),
		),
		fx.Invoke(func(s *StructUsingAllFetchers) {}),
		fx.Invoke(func(s *StructUsingSpecificFetcher) {}),
	).Run()
}

However, I find this undesirable because I know there will always be one element in the slice, even though I am forced to provide a slice of GameFetcher. It could very well be that I'm doing things wrong and I'm missing something, please let me know if there is a better way to do this!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions