Skip to main content

Introduction

Framefox provides a comprehensive Dependency Injection (DI) container that manages service instantiation, dependency resolution, and object lifecycle. The DI system eliminates manual service creation and promotes loose coupling between components.

ServiceContainer Overview

The ServiceContainer is the heart of Framefox’s dependency injection system:
/home/daytona/workspace/source/framefox/core/di/service_container.py:32-64
class ServiceContainer:
    """
    Advanced dependency injection container for the Framefox framework.

    The ServiceContainer implements a comprehensive dependency injection system with features including:
    - Automatic service discovery and registration
    - Lazy loading with background scanning capabilities
    - Circular dependency detection
    - Service caching and performance optimization
    - Memory management and cleanup
    - Factory pattern support
    - Tag-based service retrieval

    This container follows a singleton pattern and provides both eager and lazy initialization
    strategies for optimal performance.

    Key Features:
    - Autowiring: Automatic dependency resolution based on type hints
    - Service Discovery: Automatic scanning of modules for service classes
    - Caching: Intelligent caching of service definitions and instances
    - Background Scanning: Non-blocking service discovery for large projects
    - Memory Management: Cleanup capabilities to manage memory usage
    - Essential Services: Priority loading for critical framework components

    Usage:
        container = ServiceContainer()
        service = container.get(MyService)
        services = container.get_all_by_tag('controller')

    Thread Safety:
        The container is thread-safe for service retrieval but not for registration.
        Service registration should be completed during initialization.
    """

Key Features

  • Automatic Service Discovery: Scans your codebase to find and register services
  • Autowiring: Resolves dependencies automatically based on type hints
  • Singleton Pattern: Services are instantiated once and reused
  • Tag-based Retrieval: Organize and retrieve services by tags
  • Lazy Loading: Services are created only when needed
  • Circular Dependency Detection: Prevents infinite dependency loops

Retrieving Services

Basic Retrieval

Get a service by its class:
/home/daytona/workspace/source/framefox/core/di/service_container.py:548-566
def get(self, service_class: Type[Any]) -> Any:
    """Get a service instance with dependency injection."""
    if service_class in self._resolution_cache:
        return self._resolution_cache[service_class]

    if service_class in self._instances:
        cached_instance = self._instances[service_class]
        self._resolution_cache[service_class] = cached_instance
        return cached_instance

    if not inspect.isclass(service_class):
        return service_class

    instance = self._factory_manager.create_service(service_class, self)
    if instance is not None:
        self._instances[service_class] = instance
        self._resolution_cache[service_class] = instance
        return instance
Example:
from framefox.core.di.service_container import ServiceContainer
from framefox.core.logging.logger import Logger

container = ServiceContainer()
logger = container.get(Logger)
logger.info("Service retrieved successfully")

Retrieve by Name

Get a service by its class name:
/home/daytona/workspace/source/framefox/core/di/service_container.py:826-832
def get_by_name(self, class_name: str) -> Optional[Any]:
    """Retrieve a service by its class name."""
    definition = self._registry.get_definition_by_name(class_name)
    if definition:
        return self.get(definition.service_class)

    self._logger.warning(f"Service with name '{class_name}' not found")
    return None
Example:
settings = container.get_by_name("Settings")

Retrieve by Tag

Get services associated with a specific tag:
/home/daytona/workspace/source/framefox/core/di/service_container.py:834-845
def get_by_tag(self, tag: str) -> Optional[Any]:
    """Return the first service associated with a tag."""
    definitions = self._registry.get_definitions_by_tag(tag)

    if not definitions:
        return None

    if len(definitions) > 1:
        service_names = [def_.service_class.__name__ for def_ in definitions]
        self._logger.warning(f"Multiple services found for tag '{tag}': {', '.join(service_names)}. Returning first.")

    return self.get(definitions[0].service_class)
Get all services with a tag:
/home/daytona/workspace/source/framefox/core/di/service_container.py:860-863
def get_all_by_tag(self, tag: str) -> List[Any]:
    """Return all services associated with a tag."""
    definitions = self._registry.get_definitions_by_tag(tag)
    return [self.get(def_.service_class) for def_ in definitions]
Example:
# Get all controllers
controllers = container.get_all_by_tag("controller")

# Get first service with tag
template_renderer = container.get_by_tag("core.templates.template_renderer")

Automatic Dependency Injection

In Controllers

The @Route decorator automatically injects services into controller methods based on type hints:
/home/daytona/workspace/source/framefox/core/routing/decorator/route.py:27-56
@wraps(func)
async def wrapper(*args, **kwargs):
    controller_instance = args[0] if args else None

    if controller_instance and hasattr(controller_instance, "_container"):
        for param_name, param in original_sig.parameters.items():
            if param_name == "self" or param_name in kwargs:
                continue

            param_type = type_hints.get(param_name)

            if param_type and param_type != type(None):
                if (
                    self._is_fastapi_native_type(param_type)
                    or self._is_pydantic_model(param_type)
                    or self._is_primitive_type(param_type)
                    or self._is_path_parameter(param_name)
                ):
                    continue

                try:
                    service = controller_instance._container.get(param_type)
                    kwargs[param_name] = service
                except Exception as e:
                    if param.default != inspect.Parameter.empty:
                        kwargs[param_name] = param.default
                    # Handle error...

    return await func(*args, **kwargs)
Example:
from framefox.core.controller.abstract_controller import AbstractController
from framefox.core.routing.decorator.route import Route
from framefox.core.orm.entity_manager_interface import EntityManagerInterface
from framefox.core.logging.logger import Logger

class UserController(AbstractController):
    @Route("/users", "user.list", methods=["GET"])
    async def list_users(self, em: EntityManagerInterface, logger: Logger):
        # Services are automatically injected!
        logger.info("Fetching users")
        users = em.find_all(User)
        return self.render("user/list.html", {"users": users})
The Route decorator intelligently distinguishes between:
  • Services to inject: Custom services registered in the container
  • FastAPI parameters: Request, Response, Query, Path, etc.
  • Path parameters: URL parameters like {user_id}
  • Pydantic models: Request body models
  • Primitive types: int, str, bool, etc.

In Service Classes

Services can depend on other services through constructor injection:
class UserService:
    def __init__(self, logger: Logger, em: EntityManagerInterface):
        self.logger = logger
        self.em = em
    
    def create_user(self, username: str, email: str):
        self.logger.info(f"Creating user: {username}")
        user = User(username=username, email=email)
        self.em.persist(user)
        self.em.flush()
        return user
When you retrieve UserService from the container, its dependencies are automatically resolved:
user_service = container.get(UserService)
# Logger and EntityManagerInterface are automatically injected

Dependency Resolution

The container resolves dependencies using type hints:
/home/daytona/workspace/source/framefox/core/di/service_container.py:672-712
def _resolve_dependencies(self, service_class: Type[Any]) -> List[Any]:
    """Resolve the dependencies of a service class for autowiring."""
    dependencies = []

    try:
        signature = inspect.signature(service_class.__init__)

        # Get type hints safely
        try:
            type_hints = get_type_hints(service_class.__init__)
        except NameError as e:
            self._logger.warning(f"Error getting type hints for {service_class.__name__}: {e}")
            type_hints = {}

        for param_name, param in signature.parameters.items():
            if param_name == "self":
                continue

            param_type = type_hints.get(param_name)

            if param_type:
                try:
                    dependency = self.get(param_type)
                    dependencies.append(dependency)
                except Exception as dep_e:
                    if param.default != inspect.Parameter.empty:
                        dependencies.append(param.default)
                    else:
                        self._logger.warning(
                            f"Could not resolve dependency {param_name} of type {param_type} for {service_class.__name__}: {dep_e}"
                        )
            elif param.default != inspect.Parameter.empty:
                dependencies.append(param.default)
            else:
                self._logger.warning(f"Parameter {param_name} of {service_class.__name__} has no type hint and no default value")

    except Exception as e:
        self._logger.error(f"Failed to resolve dependencies for {service_class.__name__}: {e}")
        return []

    return dependencies

Circular Dependency Detection

The container detects and prevents circular dependencies:
/home/daytona/workspace/source/framefox/core/di/service_container.py:580-598
if service_class in self._circular_detection:
    chain = list(self._circular_detection)
    raise CircularDependencyError(service_class, chain)

if not definition:
    raise ServiceNotFoundError(f"Service {service_class.__name__} not found and cannot be auto-registered")

self._circular_detection.add(service_class)

try:
    instance = self._create_service_instance(definition)
    self._instances[service_class] = instance
    self._resolution_cache[service_class] = instance
    return instance
except Exception as e:
    self._logger.error(f"Failed to create service {service_class.__name__}: {e}")
    raise ServiceInstantiationError(service_class, e)
finally:
    self._circular_detection.discard(service_class)

Service Registration

Automatic Discovery

Framefox automatically discovers services in your project:
/home/daytona/workspace/source/framefox/core/di/service_container.py:225-258
def _discover_and_register_services(self) -> None:
    """Discover services with cache support and lazy loading."""

    cache_data = self._cache_manager.load_cache()
    if cache_data and self._cache_manager.load_services_from_cache(cache_data, self._registry, self._scanned_modules):
        self._logger.debug("Services loaded from cache")
        self._src_scanned = True
        return

    core_path = Path(__file__).resolve().parent.parent
    self._setup_exclusions()
    self._scan_for_service_definitions(
        core_path,
        "framefox.core",
        self._excluded_directories,
        self._excluded_modules,
    )

    self._src_paths = self._find_source_paths()
    for src_path in self._src_paths:
        for subdir in src_path.iterdir():
            if subdir.is_dir() and subdir.name not in [
                "controller",
                "controllers",
                "__pycache__",
            ]:
                self._scan_for_service_definitions(subdir, f"src.{subdir.name}", [], [])

    self._save_initial_cache()

    if self._should_use_background_scan():
        self._start_background_src_scan()
    else:
        self._src_scanned = True
Services are discovered from:
  • framefox.core - Framework core services
  • src/ - Your application services (excluding controllers, entities, migrations)

Manual Registration

You can manually set service instances:
/home/daytona/workspace/source/framefox/core/di/service_container.py:873-876
def set_instance(self, service_class: Type[Any], instance: Any) -> None:
    """Manually set a service instance."""
    self._instances[service_class] = instance
    self._resolution_cache[service_class] = instance
Example:
custom_service = MyCustomService()
container.set_instance(MyCustomService, custom_service)

Service Factories

Register custom factories for complex service creation:
/home/daytona/workspace/source/framefox/core/di/service_container.py:878-880
def register_factory(self, factory) -> None:
    """Register a service factory."""
    self._factory_manager.register_factory(factory)

Service Lifecycle

Singleton Pattern

All services in Framefox are singletons by default. Once created, the same instance is reused:
service1 = container.get(MyService)
service2 = container.get(MyService)
assert service1 is service2  # Same instance

Service Caching

The container implements two levels of caching:
  1. Instance Cache: Stores created service instances
  2. Resolution Cache: Speeds up repeated retrievals
/home/daytona/workspace/source/framefox/core/di/service_container.py:797-809
def cleanup_memory(self) -> None:
    """Clean up container memory."""
    self._resolution_cache.clear()

    essential_modules = {mod for mod in self._module_scan_cache.keys() if mod.startswith("framefox.core") or "controller" in mod}

    modules_to_remove = set(self._module_scan_cache.keys()) - essential_modules
    for module_name in modules_to_remove:
        del self._module_scan_cache[module_name]

    collected = gc.collect()

    self._logger.debug(f"Memory cleanup: removed {len(modules_to_remove)} cached modules, collected {collected} objects")

Advanced Usage

Checking Service Existence

/home/daytona/workspace/source/framefox/core/di/service_container.py:865-871
def has(self, service_class: Type[Any]) -> bool:
    """Check if a service is registered."""
    return self._registry.has_definition(service_class)

def has_by_name(self, class_name: str) -> bool:
    """Check if a service is registered by name."""
    return self._registry.has_definition_by_name(class_name)
Example:
if container.has(EmailService):
    email_service = container.get(EmailService)
    email_service.send_notification()

Container Statistics

/home/daytona/workspace/source/framefox/core/di/service_container.py:908-919
def get_stats(self) -> Dict[str, Any]:
    """Get container statistics."""
    registry_stats = self._registry.get_stats()

    return {
        "container_instance": self._instance_counter,
        "instantiated_services": len(self._instances),
        "cached_resolutions": len(self._resolution_cache),
        "registered_factories": len(self._factory_manager.get_factories()),
        **registry_stats,
    }
Example:
stats = container.get_stats()
print(f"Services instantiated: {stats['instantiated_services']}")
print(f"Cached resolutions: {stats['cached_resolutions']}")

Best Practices

Type hints are essential for autowiring to work:
# Good
def __init__(self, logger: Logger, em: EntityManagerInterface):
    self.logger = logger
    self.em = em

# Bad - won't be autowired
def __init__(self, logger, em):
    self.logger = logger
    self.em = em
Design your services to avoid circular dependencies:
# Bad
class ServiceA:
    def __init__(self, service_b: ServiceB):
        ...

class ServiceB:
    def __init__(self, service_a: ServiceA):
        ...

# Good - extract common logic
class SharedService:
    ...

class ServiceA:
    def __init__(self, shared: SharedService):
        ...

class ServiceB:
    def __init__(self, shared: SharedService):
        ...
Each service should have a single, well-defined responsibility:
# Good - focused services
class UserService:
    def create_user(self, data): ...
    def update_user(self, id, data): ...

class EmailService:
    def send_email(self, to, subject, body): ...

# Bad - too many responsibilities
class UserService:
    def create_user(self, data): ...
    def send_welcome_email(self, user): ...
    def upload_avatar(self, file): ...
    def generate_pdf_report(self, user): ...
Depend on interfaces rather than concrete implementations:
# Define interface
class EmailServiceInterface(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str): ...

# Implementations
class SmtpEmailService(EmailServiceInterface):
    def send(self, to, subject, body): ...

class SendGridEmailService(EmailServiceInterface):
    def send(self, to, subject, body): ...

# Controllers depend on interface
class UserController(AbstractController):
    async def register(self, email_service: EmailServiceInterface):
        email_service.send(...)

Troubleshooting

Service Not Found Error

If you get a ServiceNotFoundError, ensure:
  1. The service class is in the src/ directory (not in excluded directories)
  2. The class name follows Python naming conventions (starts with uppercase)
  3. The module doesn’t have import errors

Circular Dependency Error

If you encounter circular dependencies:
  1. Review your service dependencies
  2. Extract shared logic into a separate service
  3. Consider using lazy imports or factory patterns

Type Hint Issues

If autowiring isn’t working:
  1. Ensure you’re using proper type hints
  2. Check for forward reference issues
  3. Verify the service is registered in the container

Next Steps

MVC Pattern

See how DI integrates with controllers

Services

Learn to create custom services

Testing

Mock services for unit testing

Architecture

Understand the overall framework architecture

Build docs developers (and LLMs) love