Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions di.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,66 @@ func MustInvokeAs[T any](i Injector) T {
return must1(InvokeAs[T](i))
}

// InvokeAsAll invokes all services in the DI container that match the provided type or interface.
// This function searches through all registered services to find all that can be cast to the requested type T.
// Returns a slice of all matching services in deterministic order by service name.
//
// Parameters:
// - i: The injector to search for services
//
// Returns a slice of service instances and any error that occurred during invocation.
// If no services match, returns an empty slice and no error.
// If some services fail to invoke, returns successfully invoked services with an error describing the failures.
//
// Play: https://go.dev/play/p/spRqDOsXQLs
//
// Example:
//
// // Register multiple database implementations
// do.Provide(injector, func(i do.Injector) (*PostgresDB, error) {
// return &PostgresDB{}, nil
// })
// do.Provide(injector, func(i do.Injector) (*MySQLDB, error) {
// return &MySQLDB{}, nil
// })
// // Both implement Database interface
//
// // Invoke all databases
// databases, err := do.InvokeAsAll[Database](injector)
// // databases contains both PostgresDB and MySQLDB instances
func InvokeAsAll[T any](i Injector) ([]T, error) {
return invokeAsAllByGenericType[T](i)
}

// MustInvokeAsAll invokes all services in the DI container that match the provided type or interface.
// This function panics if an error occurs during invocation.
// Returns a slice of all matching service instances in deterministic order.
//
// Parameters:
// - i: The injector to search for services
//
// Returns a slice of service instances.
// Panics if any service cannot be found or invoked.
//
// Play: https://go.dev/play/p/spRqDOsXQLs
//
// Example:
//
// // Register multiple repositories
// do.Provide(injector, func(i do.Injector) (*UserRepository, error) {
// return &UserRepository{}, nil
// })
// do.Provide(injector, func(i do.Injector) (*ProductRepository, error) {
// return &ProductRepository{}, nil
// })
// // Both implement Repository interface
//
// // Invoke all repositories (panics on error)
// repositories := do.MustInvokeAsAll[Repository](injector)
func MustInvokeAsAll[T any](i Injector) []T {
return must1(InvokeAsAll[T](i))
}

/////////////////////////////////////////////////////////////////////////////
// Package-level declaration
/////////////////////////////////////////////////////////////////////////////
Expand Down
170 changes: 170 additions & 0 deletions di_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,176 @@ func TestMustInvokeAs(t *testing.T) {
})
}

func TestInvokeAsAll(t *testing.T) {
testWithTimeout(t, 100*time.Millisecond)
is := assert.New(t)

i := New()

// Register multiple services implementing the same interface
Provide(i, func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "first"}, nil
})
Provide(i, func(i Injector) (*lazyTestHeathcheckerKO, error) {
return &lazyTestHeathcheckerKO{foobar: "second"}, nil
})

// Test successful invocation of all matching services
services, err := InvokeAsAll[Healthchecker](i)
is.NoError(err)
is.Len(services, 2)

// Verify we got 2 services and they are Healthchecker interfaces
// We can't directly cast to concrete types since we get []Healthchecker
// But we can verify the services are properly instantiated by checking they're not nil
is.NotNil(services[0])
is.NotNil(services[1])

// Test empty result
emptyInjector := New()
emptyServices, err := InvokeAsAll[Healthchecker](emptyInjector)
is.NoError(err)
is.Empty(emptyServices)

// Test with named services for deterministic ordering
namedInjector := New()
ProvideNamed(namedInjector, "service-z", func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "z-service"}, nil
})
ProvideNamed(namedInjector, "service-a", func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "a-service"}, nil
})
ProvideNamed(namedInjector, "service-m", func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "m-service"}, nil
})

namedServices, err := InvokeAsAll[Healthchecker](namedInjector)
is.NoError(err)
is.Len(namedServices, 3)
// Services should be sorted alphabetically by service name: service-a, service-m, service-z
is.NotNil(namedServices[0])
is.NotNil(namedServices[1])
is.NotNil(namedServices[2])

// Test with different service types
mixedInjector := New()
ProvideNamed(mixedInjector, "lazy-service", func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "lazy"}, nil
})
ProvideNamedValue(mixedInjector, "eager-service", &lazyTestHeathcheckerOK{foobar: "eager"})
ProvideNamedTransient(mixedInjector, "transient-service", func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "transient"}, nil
})

mixedServices, err := InvokeAsAll[Healthchecker](mixedInjector)
is.NoError(err)
is.Len(mixedServices, 3)
is.NotNil(mixedServices[0])
is.NotNil(mixedServices[1])
is.NotNil(mixedServices[2])

// Test scope inheritance
scopeInjector := New()
childScope := scopeInjector.Scope("child")

// Register services in different scopes
Provide(scopeInjector, func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "root"}, nil
})
Provide(childScope, func(i Injector) (*lazyTestHeathcheckerKO, error) {
return &lazyTestHeathcheckerKO{foobar: "child"}, nil
})

// Test from root scope - should see services from root scope
rootServices, err := InvokeAsAll[Healthchecker](scopeInjector)
is.NoError(err)
is.GreaterOrEqual(len(rootServices), 1) // Should have at least the root service

// Test from child scope - should see services from both scopes
childServices, err := InvokeAsAll[Healthchecker](childScope)
is.NoError(err)
is.GreaterOrEqual(len(childServices), 1) // Should have at least one service
}

func TestMustInvokeAsAll(t *testing.T) {
testWithTimeout(t, 100*time.Millisecond)
is := assert.New(t)

i := New()
Provide(i, func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "test"}, nil
})

// Test successful invocation
is.NotPanics(func() {
services := MustInvokeAsAll[Healthchecker](i)
is.Len(services, 1)
})

// Test that MustInvokeAsAll returns empty slice when no services found (not panic)
emptyInjector := New()
emptyServices := MustInvokeAsAll[Healthchecker](emptyInjector)
is.Empty(emptyServices)

// Test with multiple services
multiInjector := New()
Provide(multiInjector, func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "multi1"}, nil
})
Provide(multiInjector, func(i Injector) (*lazyTestHeathcheckerKO, error) {
return &lazyTestHeathcheckerKO{foobar: "multi2"}, nil
})

is.NotPanics(func() {
multiServices := MustInvokeAsAll[Healthchecker](multiInjector)
is.Len(multiServices, 2)
is.NotNil(multiServices[0])
is.NotNil(multiServices[1])
})

// Test with different interfaces
interfaceInjector := New()
Provide(interfaceInjector, func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "health"}, nil
})
Provide(interfaceInjector, func(i Injector) (*lazyTestShutdownerOK, error) {
return &lazyTestShutdownerOK{foobar: "shutdown"}, nil
})

// Test Healthchecker interface
is.NotPanics(func() {
healthServices := MustInvokeAsAll[Healthchecker](interfaceInjector)
is.Len(healthServices, 1)
is.NotNil(healthServices[0])
})

// Test Shutdowner interface
is.NotPanics(func() {
shutdownServices := MustInvokeAsAll[Shutdowner](interfaceInjector)
// Length depends on interface compatibility
is.LessOrEqual(len(shutdownServices), 1)
})

// Test with service aliases (addresses concern b and c from issue #114)
aliasInjector := New()

// Register a concrete service
Provide(aliasInjector, func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "concrete"}, nil
})

// Create an alias
err := As[*lazyTestHeathcheckerOK, Healthchecker](aliasInjector)
is.NoError(err)

// Test that InvokeAsAll finds both the original service and the alias
aliasServices, err := InvokeAsAll[Healthchecker](aliasInjector)
is.NoError(err)
is.Len(aliasServices, 2) // Original service + alias
is.NotNil(aliasServices[0])
is.NotNil(aliasServices[1])
}

/////////////////////////////////////////////////////////////////////////////
// Package-level declaration
/////////////////////////////////////////////////////////////////////////////
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,7 @@ An alternative name given to a service in DI. It allows a service to be accessed

A mechanism that groups multiple service registrations into a single unit that can be imported and registered with a DI container. Package loaders use `do.Package()` to assemble services with different loading strategies (lazy, eager, transient) and can include named services and interface bindings. This allows for modular service organization where related services can be bundled together and imported as a cohesive unit, promoting modularity and reusability across different parts of an application.

## Bulk Service Invocation

A technique for retrieving all services that implement a specific interface, returning them as a slice rather than a single instance. This is useful when you need to work with multiple implementations of the same interface, such as multiple database connections, message queue processors, or storage backends.

66 changes: 62 additions & 4 deletions docs/docs/service-invocation/accept-interfaces-return-structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ Aliases must be Go interfaces. It can be declared explicitly on injection using

1. **Implicit alias invocation**:
- provide struct, invoke interface
- `do.InvokeAs()`
- `do.InvokeAs()` (single service)
- `do.InvokeAsAll()` (all matching services)
2. **Explicit alias injection**:
- provide struct, bind interface, invoke interface
- `do.As()`

## Implicit invocation (preferred)

2 methods are available for implicit invocation:
- `do.InvokeAs`
- `do.MustInvokeAs`
4 methods are available for implicit invocation:
- `do.InvokeAs` (single service)
- `do.MustInvokeAs` (single service, panics on error)
- `do.InvokeAsAll` (all matching services)
- `do.MustInvokeAsAll` (all matching services, panics on error)

Named invocation is not available for now. Feel free to open an issue to discuss your needs.

Expand Down Expand Up @@ -62,6 +65,61 @@ The first matching service in the scope tree is returned.

:::

## Bulk implicit invocation

For scenarios where you need to work with multiple services that implement the same interface, use `do.InvokeAsAll`:

```go
type Processor interface {
Process(data string) error
}

type FileProcessor struct {}
func (f *FileProcessor) Process(data string) error { return nil }

type NetworkProcessor struct {}
func (n *NetworkProcessor) Process(data string) error { return nil }

i := do.New()

// Register multiple processors
do.Provide(i, func(i do.Injector) (*FileProcessor, error) {
return &FileProcessor{}, nil
})
do.Provide(i, func(i do.Injector) (*NetworkProcessor, error) {
return &NetworkProcessor{}, nil
})

// Invoke all processors
processors, err := do.InvokeAsAll[Processor](i)
if err != nil {
log.Fatal(err)
}

// Process with all available processors
for _, processor := range processors {
processor.Process("data")
}
```

### Characteristics of InvokeAsAll

- **Returns a slice**: `[]T` instead of single `T`
- **Deterministic ordering**: Services sorted by registration name
- **Scope inheritance**: Finds services across all scopes
- **Partial failures**: Returns successful services even if some fail
- **Empty results**: Valid empty slice when no services match

### When to use InvokeAsAll vs InvokeAs

| Scenario | Use InvokeAs | Use InvokeAsAll |
|----------|--------------|-----------------|
| Single service needed | ✅ | ❌ |
| Multiple services needed | ❌ | ✅ |
| Fail-fast on missing service | ✅ | ❌ |
| Graceful handling of zero services | ❌ | ✅ |
| Load balancing across services | ❌ | ✅ |

:::warning

Invoking an implicit alias with a very simple interface signature might lead to loading the wrong service.
Expand Down
Loading