cli/azd/pkg/ioc/container.go (186 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // This package wraps the golobby/container package to provide support for the following: // 1. Easier usage of lazy type resolvers and ability to register specific type instances // 2. Support for hierarchical/nested containers to resolve types from parent containers // 3. Helper methods for easier/streamlined usage of of the IoC container package ioc import ( "errors" "fmt" "reflect" "regexp" "github.com/golobby/container/v3" ) var ( // The golobby project does not support types errors, // but all the error messages are prefixed with `container:` containerErrorRegex *regexp.Regexp = regexp.MustCompile("container:") ErrResolveInstance error = errors.New("failed resolving instance from container") ) // NestedContainer is an IoC container that support nested containers // Used for more complex registration scenarios such as scop based registration/resolution. type NestedContainer struct { inner container.Container scopedBindings []*binding } // Creates a new nested container from the specified parent container func NewNestedContainer(parent *NestedContainer) *NestedContainer { current := container.New() if parent != nil { // Copy the bindings to the new container // The bindings hold the concrete instance of singleton registrations for key, value := range parent.inner { current[key] = value } } instance := &NestedContainer{ inner: current, } RegisterInstance[ServiceLocator](instance, instance) return instance } // Creates a new container with only registrations from the given container. func NewRegistrationsOnly(from *NestedContainer) *NestedContainer { current := container.New() if from != nil { // Reset all concrete instances by copying 'resolver' and 'isSingleton' fields // Reflection is necessary since *container.binding is unexported for key, value := range from.inner { var valueType = reflect.TypeOf(value) newValue := reflect.MakeMapWithSize(valueType, len(value)) for name, binding := range value { bindingVal := reflect.ValueOf(binding).Elem() newBinding := reflect.New(reflect.TypeOf(binding).Elem()) setUnexportedField( newBinding.Elem().FieldByName("resolver"), getUnexportedField(bindingVal.FieldByName("resolver"))) setUnexportedField( newBinding.Elem().FieldByName("isSingleton"), getUnexportedField(bindingVal.FieldByName("isSingleton"))) n := name newValue.SetMapIndex(reflect.ValueOf(n), newBinding) } reflect.ValueOf(current).SetMapIndex(reflect.ValueOf(key), newValue) } } instance := &NestedContainer{ inner: current, } RegisterInstance[ServiceLocator](instance, instance) return instance } func getUnexportedField(field reflect.Value) interface{} { return reflect.NewAt(field.Type(), field.Addr().UnsafePointer()).Elem().Interface() } func setUnexportedField(field reflect.Value, value interface{}) { reflect.NewAt(field.Type(), field.Addr().UnsafePointer()). Elem(). Set(reflect.ValueOf(value)) } // Fill takes a structure and resolves fields with the tag `container:"type" or `container:"name"`. func (c *NestedContainer) Fill(structure any) error { return c.inner.Fill(structure) } // Registers a resolver with a singleton lifetime // Returns an error if the resolver is not valid func (c *NestedContainer) RegisterSingleton(resolveFn any) error { return c.inner.SingletonLazy(resolveFn) } // Registers a resolver with a singleton lifetime // Panics if the resolver is not valid func (c *NestedContainer) MustRegisterSingleton(resolveFn any) { container.MustSingletonLazy(c.inner, resolveFn) } // Registers a resolver with a singleton lifetime and instantiates the instance // Instance is stored in container cache is used for future resolutions // Returns an error if the resolver cannot instantiate the type func (c *NestedContainer) RegisterSingletonAndInvoke(resolveFn any) error { return c.inner.Singleton(resolveFn) } // Registers a named resolver with a singleton lifetime // Returns an error if the resolver is not valid func (c *NestedContainer) RegisterNamedSingleton(name string, resolveFn any) error { return c.inner.NamedSingletonLazy(name, resolveFn) } // Registers a named resolver with a singleton lifetime // Panics if the resolver is not valid func (c *NestedContainer) MustRegisterNamedSingleton(name string, resolveFn any) { container.MustNamedSingletonLazy(c.inner, name, resolveFn) } // Registers a resolver with a transient lifetime (instance per resolution) // Returns an error if the resolver is not valid func (c *NestedContainer) RegisterTransient(resolveFn any) error { return c.inner.TransientLazy(resolveFn) } // Registers a named resolver with a singleton lifetime and instantiates the instance // Panics if the resolver is not valid func (c *NestedContainer) MustRegisterTransient(resolveFn any) { container.MustTransientLazy(c.inner, resolveFn) } // Registers a named resolver with a transient lifetime (instance per resolution) // Returns an error if the resolver is not valid func (c *NestedContainer) RegisterNamedTransient(name string, resolveFn any) error { return c.inner.NamedTransientLazy(name, resolveFn) } // Registers a named resolver with a transient lifetime (instance per resolution) // Panics if the resolver is not valid func (c *NestedContainer) MustRegisterNamedTransient(name string, resolveFn any) { container.MustNamedTransientLazy(c.inner, name, resolveFn) } // Registers a resolver with a scoped lifetime (instance per scope) // Ex: Each new cobra command will create a new scope // Scoped registrations are added as singletons in the current container then are reset in any new child containers // Returns an error if the resolver is not valid func (c *NestedContainer) RegisterScoped(resolveFn any) error { if err := c.inner.SingletonLazy(resolveFn); err != nil { return err } c.scopedBindings = append(c.scopedBindings, &binding{ resolver: resolveFn, }) return nil } // Registers a resolver with a scoped lifetime (instance per scope) // Ex: Each new cobra command will create a new scope // Scoped registrations are added as singletons in the current container then are reset in any new child containers // Panics if the resolver is not valid func (c *NestedContainer) MustRegisterScoped(resolveFn any) { if err := c.RegisterScoped(resolveFn); err != nil { panic(err) } } // Registers a named resolver with a scoped lifetime (instance per scope) // Ex: Each new cobra command will create a new scope // Scoped registrations are added as singletons in the current container then are reset in any new child containers func (c *NestedContainer) RegisterNamedScoped(name string, resolveFn any) error { if err := c.inner.NamedSingletonLazy(name, resolveFn); err != nil { return err } c.scopedBindings = append(c.scopedBindings, &binding{ name: name, resolver: resolveFn, }) return nil } // Registers a named resolver with a scoped lifetime (instance per scope) // Ex: Each new cobra command will create a new scope // Scoped registrations are added as singletons in the current container then are reset in any new child containers // Panics if the resolver is not valid func (c *NestedContainer) MustRegisterNamedScoped(name string, resolveFn any) { if err := c.RegisterNamedScoped(name, resolveFn); err != nil { panic(err) } } // Resolves an instance for the specified type // Returns an error if the resolution fails func (c *NestedContainer) Resolve(instance any) error { if err := c.inner.Resolve(instance); err != nil { return inspectResolveError(err) } return nil } // Resolves a named instance for the specified type // Returns an error if the resolution fails func (c *NestedContainer) ResolveNamed(name string, instance any) error { if err := c.inner.NamedResolve(instance, name); err != nil { return inspectResolveError(err) } return nil } // Invokes the specified function and resolves any arguments specified // from the container resolver registrations func (c *NestedContainer) Invoke(resolver any) error { return c.inner.Call(resolver) } // Registers a constructed instance of the specified type // Panics if the registration fails func RegisterInstance[F any](c *NestedContainer, instance F) { container.MustSingletonLazy(c.inner, func() F { return instance }) } // Registers a named constructed instance of the specified type // Panics if the registration fails func RegisterNamedInstance[F any](c *NestedContainer, name string, instance F) { container.MustNamedSingletonLazy(c.inner, name, func() F { return instance }) } // NewScope creates a new nested container with a relationship back to the parent container // Scoped registrations are converted to singleton registrations within the new nested container. func (c *NestedContainer) NewScope() (*NestedContainer, error) { childContainer := NewNestedContainer(c) for _, binding := range c.scopedBindings { if binding.name == "" { if err := childContainer.RegisterSingleton(binding.resolver); err != nil { return nil, err } } else { if err := childContainer.RegisterNamedSingleton(binding.name, binding.resolver); err != nil { return nil, err } } childContainer.scopedBindings = append(childContainer.scopedBindings, binding) } return childContainer, nil } // NewScopeRegistrationsOnly creates a new container with bindings deep copied from the container. // Scoped registrations are then activated as singletons within the new nested container. func (c *NestedContainer) NewScopeRegistrationsOnly() (*NestedContainer, error) { childContainer := NewRegistrationsOnly(c) for _, binding := range c.scopedBindings { if binding.name == "" { if err := childContainer.RegisterSingleton(binding.resolver); err != nil { return nil, err } } else { if err := childContainer.RegisterNamedSingleton(binding.name, binding.resolver); err != nil { return nil, err } } childContainer.scopedBindings = append(childContainer.scopedBindings, binding) } return childContainer, nil } // Inspects the specified error to determine whether the error is a // developer container registration error or an error that was // returned while instantiating a dependency. func inspectResolveError(err error) error { if containerErrorRegex.Match([]byte(err.Error())) { return fmt.Errorf("%w: %w", ErrResolveInstance, err) } return err }