-
Notifications
You must be signed in to change notification settings - Fork 0
Dependency Injection
type Resolver interface {
Resolve(target any) error
ResolveAll(target any) error
}
type Container interface {
Resolver
Singleton(factory any) error
Scoped(factory any) error
Transient(factory any) error
Instance(v any) error
Unbind(v any) (bool, error)
}
type Scope interface {
Resolver
Close() error
}Declared in rextension/di and re-exported by rextension, so extension code
writes rextension.Resolver and never imports an implementation.
This is the point of the package. The contract mirrors
dix, which implements it — but an
extension that names dix.Container acquires a dependency it uses in zero
Go files, purely because the framework hands one back. Six extensions in this
ecosystem carried exactly that.
The practical consequence, beyond a tidier module graph: a change to dix's
Container interface stops reaching extension code — including test mocks,
which is precisely the churn adding Unbind caused.
Why
diis its own package. It is a leaf; it imports nothing. Bothrextensionandrextension/routeneed these types, andrextensionalready importsrextension/route, so declaring them in either one would be an import cycle.
A health check, a route handler and a factory all need to read dependencies, not register them. Ask for the narrowest thing that works:
func (c *DBCheck) Check(ctx context.Context, r rextension.Resolver) error {
var db *Database
if err := r.Resolve(&db); err != nil {
return err
}
return db.Ping(ctx)
}An extension that offers something to the rest of the application registers it
in OnInitialize:
func (e *Extension) OnInitialize(ctx context.Context, r rextension.Rex) error {
return r.Container().Singleton(func() *myext.Registry {
return e.registry
})
}Register the concrete type and let interface matching find it. Registering an interface type makes that interface the registered identity, which is rarely what you want.
Registering the same type twice returns dix.ErrAlreadyRegistered — so an
extension that might be registered twice, or that replaces a default, uses
Unbind first:
_, _ = r.Container().Unbind(oldValue) // keys on the exact dynamic type
_ = r.Container().Instance(newValue)Unbind does not close what it removes, and singletons already handed out are
unaffected.
The framework creates one scope per request and closes it when the handler
returns. In a handler, ctx.Resolver() is that scope:
func handler(ctx rxroute.Context) {
var uow *UnitOfWork
if err := ctx.Resolver().Resolve(&uow); err != nil {
rextension.WriteProblem(ctx.ResponseWriter(), ctx.Request(),
500, rextension.ProblemInternal, "the request could not be completed")
return
}
// uow.Close() runs when the request ends
}Anything registered Scoped with a Close() error method is released there. An
extension does not create scopes and does not close them.
| Kind | Built | Closed by the container |
|---|---|---|
Singleton |
once, lazily, shared | no |
Scoped |
once per scope |
yes, on Scope.Close
|
Transient |
every resolve | no |
Instance |
by you | no |
Factories take either no arguments or a single Resolver, and return exactly
one value. For a Scoped factory, the Resolver handed in is the scope —
use it, never a captured container, or the factory's own scoped dependencies
will be resolved from the root and fail.
Full detail: dix → Lifetimes.
rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem