Service Container
Service Container
Section titled “Service Container”The Service Container in Orionis Framework is a robust solution for dependency management in your applications. Its flexible architecture allows you to register and resolve services efficiently, promoting collaboration between components without unnecessary coupling.
Advantages of using the service container
Section titled “Advantages of using the service container”- Automatic dependency injection: The container creates and manages instances for you, eliminating the need to handle dependencies manually.
- Modular and scalable design: Facilitates the development of clean and maintainable applications, where each component is independent and reusable.
- Advanced lifecycle management: Allows you to register services as singleton, scoped, or transient, adapting the lifecycle according to your application’s requirements.
- Intelligent dependency resolution: Automatically analyzes and resolves the dependencies needed for each service.
The service container in Orionis Framework is inspired by solutions from well-known frameworks such as Laravel (PHP), Symfony (PHP), Spring (Java), and .NET Core (C#), offering an optimized experience tailored for the Orionis ecosystem.
What is a service container?
Section titled “What is a service container?”A service container is a central component in software architecture that manages the creation, configuration, and lifecycle of objects and their dependencies. It acts as a centralized registry where services (classes or components) and their relationships can be defined, allowing dependencies to be injected automatically when requested.
Main features
Section titled “Main features”- Inversion of Control (IoC): The container takes control of object creation instead of objects creating themselves.
- Dependency Injection (DI): Objects receive their dependencies from the outside rather than creating them internally.
- Automatic lifecycle management: The container decides when to create, maintain, and destroy service instances.
- Decoupling: Reduces dependency between classes, making maintenance and testing easier.
What lifecycles does the service container support?
Section titled “What lifecycles does the service container support?”The service container in Orionis Framework supports three lifecycles for registered services, adapting to different application needs:
Singleton
Section titled “Singleton”A single instance of the service is created and shared throughout the application. This instance remains in memory for the entire duration of the application’s execution.
When to use:
- Configuration services
- Logging services
- Stateless services
Scoped
Section titled “Scoped”A new instance of the service is created for each specific scope or context. By default, this means one instance per HTTP request in web applications.
When to use:
- Services that maintain state during a request
- Authentication services
- User context services
Transient
Section titled “Transient”Each time the service is requested, a new instance is created. This is the lightest lifecycle in terms of memory management.
When to use:
- Lightweight, stateless services
- Calculation or processing services
- Services that do not require persistence
What is required to register a service?
Section titled “What is required to register a service?”To register a service in the Orionis Framework service container, two mandatory components are required:
- Contract (Interface): Specifies the functionality the service must implement, but does not define how it is implemented. It defines “what” the service should do.
- Implementation (Class): Provides the concrete logic that fulfills the contract defined by the interface. It defines “how” the work is done.
Benefits of this separation:
Section titled “Benefits of this separation:”- Flexibility: Allows changing the implementation without affecting the code that uses the service
- Testability: Makes it easier to create mocks and stubs for unit testing
- Maintainability: Code becomes easier to maintain and extend
Below is a basic and clear example of how to define and register a service in the Orionis Framework service container.
Service Definition
Section titled “Service Definition”Contract (Interface)
from abc import ABC, abstractmethod
class IEmailService(ABC):
@abstractmethod def configure(self, subject: str, body: str, to: str) -> None: """Configures the email parameters.""" pass
@abstractmethod def send(self) -> bool: """Sends the email and returns True if successful.""" passImplementation (Class)
from module import IEmailService
class EmailService(IEmailService):
def configure(self, subject: str, body: str, to: str) -> None: """Configures the email parameters.""" self._subject = subject self._body = body self._to = to
def send(self) -> bool: """Sends the email and returns True if successful.""" # Here would go the actual sending logic using SMTP return TrueImportant: For the service registration to be successful, the implementation class must comply with the contract defined by the interface. This ensures that all expected functionalities are present and correctly implemented. If the contract is not fully met, the service container will throw an exception indicating the breach.
How to register a service in the container?
Section titled “How to register a service in the container?”Singleton
Section titled “Singleton”To register a service with a singleton lifecycle, use the singleton method available on the application instance. With this lifecycle, a single instance of the service will be created and reused throughout the application.
Method signature
Section titled “Method signature”The signature of the singleton method is as follows:
(method) def singleton( abstract: (...) -> Any, concrete: (...) -> Any, *, alias: str = None, enforce_decoupling: bool = False) -> bool | NoneParameters
Section titled “Parameters”abstract: The interface or abstract class that defines the service contract.concrete: The concrete class that implements the service.alias(optional): An alternative name to register the service. Must be a string.enforce_decoupling(optional): If set toTrue, the container will verify that the concrete class fulfills the contract defined by the interface, but without requiring direct implementation in the class, promoting greater decoupling. Rarely used in practice, however,Orionisis flexible enough to allow it.
Usage example
Section titled “Usage example”from orionis.foundation.application import Application, IApplication
# Create the application instanceapp: IApplication = Application()
# Register the service as singletonapp.singleton(IEmailService, EmailService)
# Start the applicationapp.create()Registration with alias
Section titled “Registration with alias”If you want to use an alias to register the service:
from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register with alias (use named parameter)app.singleton(IEmailService, EmailService, alias="EmailServiceProvider")
app.create()Important: The
aliasparameter must be passed as a named argument. Passing it as the third positional parameter will result in a type error.
Scoped
Section titled “Scoped”To register a service with a scoped lifecycle, use the scoped method available on the application instance. With this lifecycle, a new instance of the service will be created for each specific scope or context (by default, each HTTP or Console request).
Method signature
Section titled “Method signature”The signature of the scoped method is as follows:
(method) def scoped( abstract: (...) -> Any, concrete: (...) -> Any, *, alias: str = None, enforce_decoupling: bool = False) -> bool | NoneParameters
Section titled “Parameters”The parameters are identical to those of the singleton method:
abstract: The interface or abstract class that defines the service contract.concrete: The concrete class that implements the service.alias(optional): An alternative name to register the service.enforce_decoupling(optional): If set toTrue, the container will verify that the concrete class fulfills the contract defined by the interface, but without requiring direct implementation in the class, promoting greater decoupling. Rarely used in practice, however,Orionisis flexible enough to allow it.
Usage example
Section titled “Usage example”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register the service as scopedapp.scoped(IEmailService, EmailService)
app.create()Registration with alias
Section titled “Registration with alias”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register with aliasapp.scoped(IEmailService, EmailService, alias="EmailServiceProvider")
app.create()Transient
Section titled “Transient”To register a service with a transient lifecycle, use the transient method available on the application instance. With this lifecycle, a new instance of the service will be created every time it is requested.
Method signature
Section titled “Method signature”The signature of the transient method is as follows:
(method) def transient( abstract: (...) -> Any, concrete: (...) -> Any, *, alias: str = None, enforce_decoupling: bool = False) -> bool | NoneParameters
Section titled “Parameters”The parameters are identical to the previous methods:
abstract: The interface or abstract class that defines the service contract.concrete: The concrete class that implements the service.alias(optional): An alternative name to register the service.enforce_decoupling(optional): If set toTrue, the container will verify that the concrete class fulfills the contract defined by the interface, but without requiring direct implementation in the class, promoting greater decoupling. Rarely used in practice, however,Orionisis flexible enough to allow it.
Usage example
Section titled “Usage example”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register the service as transientapp.transient(IEmailService, EmailService)
app.create()Registration with alias
Section titled “Registration with alias”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register with aliasapp.transient(IEmailService, EmailService, alias="EmailServiceProvider")
app.create()Other features of the service container
Section titled “Other features of the service container”Although the main methods for registering services are singleton, scoped, and transient, the Orionis Framework service container offers additional functionalities to enhance dependency management:
Instances
Section titled “Instances”You can register a specific instance of a service using the instance method. This is useful when you already have a created instance and want the container to use it.
Method signature
Section titled “Method signature”The signature of the instance method is as follows:
(method) def instance( abstract: (...) -> Any, instance: Any, *, alias: str = None, enforce_decoupling: bool = False) -> bool | NoneParameters
Section titled “Parameters”The parameters are identical to the previous methods:
abstract: The interface or abstract class that defines the service contract.instance: The specific instance of the service you want to register, already initialized.alias(optional): An alternative name to register the service.enforce_decoupling(optional): If set toTrue, the container will verify that the concrete class fulfills the contract defined by the interface, but without requiring direct implementation in the class, promoting greater decoupling. Rarely used in practice, however,Orionisis flexible enough to allow it.
Is this a Singleton?
Section titled “Is this a Singleton?”Registering a specific instance with the instance method can be considered similar to a singleton in the sense that the same instance is reused every time the service is requested. However, the key difference is that with instance, you provide the already created instance, while with singleton, the container is responsible for creating and managing the instance.
Usage example
Section titled “Usage example”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register a specific instanceapp.instance(IEmailService, EmailService())
app.create()Registration with alias
Section titled “Registration with alias”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register an instance with aliasapp.instance(IEmailService, EmailService(), alias="EmailServiceProvider")
app.create()Scoped Instance
Section titled “Scoped Instance”You can register a specific instance of a service with a scoped lifecycle using the scopedInstance method. This is useful when you want a particular instance to be used within a specific scope.
As you can see, this is different from instance, since instance is a global instance reused throughout the application, while scopedInstance is an instance reused only within a specific scope.
(method) def scopedInstance( abstract: (...) -> Any, instance: Any, *, alias: str = None, enforce_decoupling: bool = False) -> bool | NoneParameters
Section titled “Parameters”The parameters are identical to the previous methods:
abstract: The interface or abstract class that defines the service contract.instance: The specific instance of the service you want to register, already initialized.alias(optional): An alternative name to register the service.enforce_decoupling(optional): If set toTrue, the container will verify that the concrete class fulfills the contract defined by the interface, but without requiring direct implementation in the class, promoting greater decoupling. Rarely used in practice, however,Orionisis flexible enough to allow it.
Usage example
Section titled “Usage example”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register a specific instance as scopedapp.scopedInstance(IEmailService, EmailService())
app.create()Registration with alias
Section titled “Registration with alias”from orionis.foundation.application import Application, IApplication
app: IApplication = Application()
# Register a scoped instance with aliasapp.scopedInstance(IEmailService, EmailService(), alias="EmailServiceProvider")
# Start the applicationapp.create()Callable
Section titled “Callable”Can you register a service using a function? Yes, it is possible to register a service using a function or any callable object. This is useful when you need to customize the creation of the service, for example, by applying dynamic configurations or additional logic before instantiating it.
Recommendation: Although callables offer flexibility, it is recommended to register services using classes to maintain a clear and coherent architecture. The use of functions as services should be reserved for very specific cases.
Important limitations:
- The container will automatically inject the dependencies required by the callable.
- It cannot be used with singleton or scoped lifecycles due to the dynamic nature of callables.
Use this option only when you really need to manually control the creation of the service.
Method signature
Section titled “Method signature”(method) def callable( fn: (...) -> Any, *, alias: str) -> bool | NoneParameters
Section titled “Parameters”fn: The function or callable that creates and returns the service instance.alias: A mandatory alternative name to register the service.
Example
Section titled “Example”Suppose you have a function that reports errors by sending an email. You can register this function as a callable service in the container:
def report_error(email_service: IEmailService, logger: ILoggerService, error_message: str) -> bool: email_service.configure( subject='Application Error', body=error_message, to='raulmauriciounate@gmail.com' ) return email_service.send()Then, register the function in the container using the callable method:
from orionis.foundation.application import Application, IApplicationfrom app.helpers import report_error
app: IApplication = Application()
# Register the function as a callable service with aliasapp.callable(report_error, alias="report_error")
# Start the applicationapp.create()This way, you can inject and reuse the report_error function anywhere in your application, taking advantage of the container’s automatic dependency resolution.
Best practices
Section titled “Best practices”To make the most of the Orionis Framework service container, consider the following best practices when defining and registering your services:
1. Interface naming
Section titled “1. Interface naming”Use the “I” prefix for interfaces, followed by the service name:
class IEmailService(ABC): passclass IUserService(ABC): passclass ILoggerService(ABC): pass2. Use of Service Providers
Section titled “2. Use of Service Providers”Register related services in dedicated service providers to keep your code organized and modular. See the Service Providers section for more details.
3. Choosing the correct lifecycle
Section titled “3. Choosing the correct lifecycle”- Singleton: For services that are expensive to create or maintain global state
- Scoped: For services that need to maintain state during an operation
- Transient: For lightweight, stateless services
4. Avoid circular dependencies
Section titled “4. Avoid circular dependencies”Make sure your services do not depend on each other in a circular way, as this can cause issues during resolution.
How to resolve a registered service
Section titled “How to resolve a registered service”Once a service has been registered in the service container, you can resolve and inject it anywhere in your application using the container’s automatic dependency injection functionality.
In a class constructor
Section titled “In a class constructor”The most common way to resolve and inject a registered service is through a class constructor. The service container will automatically analyze the required dependencies and provide the corresponding instances when a class instance is created.
This makes it very simple and clean to use services in your controllers, services, or other application components.
class UserController(Controller):
def __init__( self, email_service: IEmailService, logger: ILoggerService ) -> None: """ email_service (IEmailService): Service for sending emails. logger (ILoggerService): Service for logging events and errors. """ self._email_service = email_service self._logger = logger
def sendWelcomeEmail( self, user_email: str ) -> bool: """ Sends a welcome email to the specified user. Configures the email with default subject and body, and sends it to the provided email. Returns True if the sending was successful, False otherwise. """
# Configure the already injected email service self._email_service.configure( subject='Welcome to Orionis Framework', body='Thank you for registering!', to=user_email )
# Send the email and log the result result = self._email_service.send()
# Log the result using the injected logging service if result: self._logger.log(f'Welcome email sent to {user_email}') else: self._logger.log(f'Failed to send welcome email to {user_email}')
# Return the sending result return resultWhat happens here?
Section titled “What happens here?”Well, the Orionis Framework dependency container automatically resolves the IEmailService and ILoggerService dependencies when an instance of UserController is created. There is no need to manually instantiate these services; the container injects them automatically, making dependency management easier and promoting a clean, decoupled design.
Simply create an instance of UserController and the container will handle the rest.
In Class Methods
Section titled “In Class Methods”You can inject dependencies directly into your class methods using the Orionis Framework service container. This is especially useful for functions or methods that require specific services without needing to store them as class attributes.
Here is an example of how to do this:
class UserController(Controller):
def sendWelcomeEmail( self, email_service: IEmailService, user_email: str ) -> bool: """ Sends a welcome email to the specified user. Configures the email with default subject and body, and sends it to the provided email. Returns True if the sending was successful, False otherwise. """
# Configure the already injected email service email_service.configure( subject='Welcome to Orionis Framework', body='Thank you for registering!', to=user_email )
# Send the email and return the result return email_service.send()In this example, the sendWelcomeEmail method receives an instance of IEmailService as a parameter. The service container automatically injects the correct implementation when the method is called, allowing you to use the service without needing to store it as a class attribute.
You only need to pass the other required parameters to the method, and the container will manage the dependencies for you.
Manually resolve a service
Section titled “Manually resolve a service”If you need to manually resolve a registered service, you can do so using the make method available on the instance or on the facade orionis.support.facades.application.Application of the application instance. This method allows you to obtain an instance of the service registered in the container.
You can resolve it using either the contract (interface) or the alias with which it was registered.
Resolving With The Application Facade
Section titled “Resolving With The Application Facade”Usage example:
from orionis.support.facades.application import Applicationfrom module import IEmailService
# Resolve the service using the contract (interface)email_service: IEmailService = Application.make(IEmailService)
# Resolve the service using the aliasemail_service_alias: IEmailService = Application.make("EmailServiceProvider")Resolving With The Application Instance
Section titled “Resolving With The Application Instance”Usage example:
from bootstrap.app import appfrom module import IEmailService
# Resolve the service using the contract (interface)email_service: IEmailService = app.make(IEmailService)
# Resolve the service using the aliasemail_service_alias: IEmailService = app.make("EmailServiceProvider")Here, we are typing the variable email_service as IEmailService to indicate that we expect an instance implementing that interface. The service container will provide the correct implementation previously registered.
Resolve a callable service
Section titled “Resolve a callable service”You can resolve a service registered as a callable using the make method in the same way as with other services. The service container will execute the callable and automatically provide the necessary dependencies.
Resolving With The Application Facade
Section titled “Resolving With The Application Facade”Usage example:
from orionis.support.facades.application import Application
# Always resolve using the aliasemail_service_alias = Application.make( "report_error", error_message="Error connecting to the database")Resolving With The Application Instance
Section titled “Resolving With The Application Instance”Usage example:
from bootstrap.app import app
# Always resolve using the aliasemail_service_alias = app.make( "report_error", error_message="Error connecting to the database")In this example, we are resolving the callable registered with the alias "report_error" and passing an error message as an additional argument. The container will automatically inject the dependencies required by the report_error function and execute the function with the provided parameters.
Validate service registration
Section titled “Validate service registration”If you need to check whether a service has been registered in the service container, you can use the bound method available on the application instance. This method allows you to verify if a specific service is registered, either by its contract (interface) or by its alias.
Method signature
Section titled “Method signature”(method) def bound( abstract_or_alias: Any) -> boolParameters
Section titled “Parameters”abstract_or_alias: The interface, abstract class, or alias of the service you want to check.
Example usage
Section titled “Example usage”# Check if the service is registered using the contract (interface)is_registered = app.bound(IEmailService)
# Check if the service is registered using the aliasis_registered_alias = app.bound("EmailServiceProvider")Get a registered service
Section titled “Get a registered service”If you need to obtain detailed information about a service registered in the container, you can use the getBinding method available on the application instance. This method returns an instance of orionis.container.entities.binding.Binding that allows you to access the complete definition of the service, including its lifecycle, implementation, and other configurations.
# Get the service using the contract (interface)service = app.getBinding(IEmailService)
# Get the service using the aliasservice = app.getBinding("EmailServiceProvider")
# Access the details of the registered serviceprint(service)
# Example of expected output# Binding(# contract=...,# concrete=...,# instance=...,# function=...,# lifetime=...,# enforce_decoupling=...,# alias=...# )Remove a registered service
Section titled “Remove a registered service”If you need to remove a service registered in the service container, you can use the drop method available on the application instance. This method allows you to delete a specific service, either by its contract (interface) or by its alias.
Method signature
Section titled “Method signature”(method) def drop( self, abstract: Callable[..., Any] = None, alias: str = None) -> boolParameters
Section titled “Parameters”abstract(optional): The interface or abstract class of the service you want to remove.alias(optional): The alias of the service you want to remove.
Example usage
Section titled “Example usage”# Remove the service using the contract (interface)app.drop(abstract=IEmailService)
# Remove the service using the aliasapp.drop(alias="EmailServiceProvider")Manually create a scope
Section titled “Manually create a scope”In advanced scenarios, you may need to manually create a new scope. This is useful when you want to explicitly manage the lifecycle of services, especially in contexts where it is not handled automatically, such as background tasks or custom processes.
Although Orionis Framework automatically manages scopes in HTTP and console requests, you can manually create a new scope using the createContext method available on the application instance.
Example usage
Section titled “Example usage”# Manually create a new scopewith app.createContext():
# Within this block, a new scope is created email_service: IEmailService = app.make(IEmailService)All services registered with a scoped lifecycle within the with block will share the same instance during the context’s duration. When exiting the block, the scope will be closed and the scoped instances will be released.
Make sure you understand scope management well to avoid memory issues or references to instances that are no longer valid outside the created context.
Resolve dependencies of a Binding
Section titled “Resolve dependencies of a Binding”If you need to resolve the dependencies of a service registered in the container, you can use the resolveDependencies method available on the application instance. This way, the container will automatically analyze and resolve all the dependencies required for the specified service.
Method signature
Section titled “Method signature”(method) def resolve( self, binding: Binding, *args, **kwargs) -> AnyParameters
Section titled “Parameters”binding: TheBindinginstance representing the service registered in the container.*args: Additional positional arguments that may be required to resolve dependencies.**kwargs: Additional named arguments that may be required to resolve dependencies.
Example usage
Section titled “Example usage”# Get the binding of the servicebinding = app.getBinding(IEmailService)
# Resolve the dependencies of the serviceemail_service: IEmailService = app.resolve(binding)Call a method with dependency injection
Section titled “Call a method with dependency injection”If you need to call a specific method of a class and want the service container to automatically inject the required dependencies for that method, you can use the call method available on the application instance. This is especially useful when you want to execute a method without manually instantiating the class or managing its dependencies.
Method signature
Section titled “Method signature”(method) def call( self, instance: Any, method_name: str, *args, **kwargs) -> AnyParameters
Section titled “Parameters”instance: The instance of the class containing the method you want to call.method_name: The name of the method you want to execute.*args: Additional positional arguments that may be required for the method.**kwargs: Additional named arguments that may be required for the method.
Example usage
Section titled “Example usage”# Create an instance of the classuser_controller = UserController()
# Call the method with dependency injectionresult = app.call(user_controller, "sendWelcomeEmail", user_email="webmaster@domain.co")Asynchronous Variant
Section titled “Asynchronous Variant”If the method you want to call is asynchronous, you can use the callAsync method available on the application instance. This allows you to execute asynchronous methods with automatic dependency injection. Its signature and usage are similar to the call method, but it is designed to work with asynchronous functions; however, even if the method is asynchronous, the call method will also work correctly.
Execute From Outside the Container
Section titled “Execute From Outside the Container”Resolve functions (Callable)
Section titled “Resolve functions (Callable)”In situations where you need to execute a function or method from outside the service container but still want to take advantage of automatic dependency injection, you can use the invoke method available on the application instance. This is useful for executing standalone functions that require services managed by the container.
Method signature
Section titled “Method signature”(method) def invoke( self, fn: Callable, *args, **kwargs) -> AnyParameters
Section titled “Parameters”fn: The function or method you want to execute.*args: Additional positional arguments that may be required for the function.**kwargs: Additional named arguments that may be required for the function.
Example usage
Section titled “Example usage”# Example function to executedef log_error(logger: ILoggerService, message: str) -> None: logger.error(message)
# Execute the function with dependency injectionresult = app.invoke( log_error, message="Critical system error")Asynchronous Variant
Section titled “Asynchronous Variant”If the function you want to execute is asynchronous, you can use the invokeAsync method available on the application instance. This allows you to execute asynchronous functions with automatic dependency injection. Although the invoke method will also work correctly with asynchronous functions, invokeAsync is optimized for this purpose.
Resolving Classes
Section titled “Resolving Classes”If you need to create an instance of a class from outside the service container, but want the container to handle automatic dependency injection, you can use the build method available on the application instance. This is useful for instantiating classes that require services managed by the container.
Method signature
Section titled “Method signature”(method) def build( self, type_: Callable[..., Any], *args, **kwargs) -> AnyParameters
Section titled “Parameters”type_: The class you want to instantiate.*args: Additional positional arguments that may be required for the class constructor.**kwargs: Additional named arguments that may be required for the class constructor.
Usage example
Section titled “Usage example”# Create an instance of UserController with dependency injectionuser_controller: UserController = app.build(UserController)