scope-local-registrations.md 7.7 KB

Plan: Scope-Local Registrations

Overview

Add the ability to register items directly on a Scope, enabling per-scope dependency injection patterns such as injecting a RequestContext in web request handling scenarios.

Requirements Summary

Based on user clarification:

  1. Child scope isolation: Child scopes created via create_child_scope() should NOT see parent scope's local registrations
  2. Registration API:
    • register_scoped(typeof(RequestContext), request_context) - register an existing instance
    • register_transient(typeof(RequestContext), () => new RequestContext()) - register with factory delegate
  3. Resolution priority: Scope-local registrations take precedence over container registrations for resolve, but resolve_all returns both

Current Architecture

classDiagram
    class Container {
        -Catalogue~Type,Registration~ registrations
        -Dictionary~Registration,Factory~ factories
        -Dictionary~Registration,Object~ singletons
        +register Type, FactoryDelegate
        +create_scope Scope
    }
    
    class Scope {
        -Container container
        -Dictionary~Registration,Object~ scoped_instances
        +resolve_type Type
        +resolve_all_type Type
        +create_child_scope Scope
    }
    
    class Registration {
        +Type implementation_type
        +Lifecycle lifecycle
        +as_type Type
        +with_lifecycle Lifecycle
        +build
    }
    
    class Factory {
        <<interface>>
        +create Scope Object
    }
    
    Container --> Scope : creates
    Scope --> Container : references
    Container --> Registration : stores
    Container --> Factory : stores
    Registration --> Lifecycle

Proposed Changes

1. Add Scope-Local Storage

Add fields to Scope.vala to store scope-local registrations and factories:

// Scope-local registrations keyed by service type
private Catalogue<Type, Registration> local_registrations;

// Scope-local factories keyed by registration
private Dictionary<Registration, Factory> local_factories;

// Counter for generating unique registration IDs
private int local_registration_counter;

2. Create Scope-Local Registration Class

Create a lightweight registration approach for scope-local items. Since these are simpler than container registrations, we can use a minimal approach:

private class LocalRegistration : Registration {
    public LocalRegistration(Type service_type, Scope scope) {
        // Simple registration that only serves one type
    }
}

Alternatively, reuse the existing Registration class but manage it separately within the Scope.

3. Add Registration Methods to Scope

register_instance - Register an existing instance

/**
 * Registers an existing instance in this scope.
 * 
 * The instance will be returned when the specified service type is resolved.
 * This registration is only visible within this scope, not in child scopes.
 * 
 * @param service_type The type to register the instance as
 * @param instance The instance to register
 */
public void register_instance(Type service_type, Object instance);

Implementation approach:

  • Create a simple Registration for the service type
  • Create a Factory that always returns the provided instance
  • Store in local_registrations and local_factories
  • Also store in scoped_instances for immediate availability

register_transient - Register with factory delegate

/**
 * Registers a transient factory in this scope.
 * 
 * A new instance will be created each time the service type is resolved.
 * This registration is only visible within this scope, not in child scopes.
 * 
 * @param service_type The type to register
 * @param factory_func The factory delegate to create instances
 */
public void register_transient(Type service_type, owned FactoryDelegate factory_func);

4. Modify Resolution Logic

Update resolve_type and resolve_registration

Current flow:

  1. Get registration from container
  2. Resolve based on lifecycle

New flow:

  1. Check scope-local registrations first
  2. If found, use scope-local factory
  3. If not found, fall back to container registrations

    public Object resolve_type(Type service_type) throws Error {
    // Check scope-local first
    if (this.local_registrations.has(service_type)) {
        var registration = this.local_registrations.get_any(service_type);
        return this.resolve_local_registration(registration, service_type);
    }
    // Fall back to container
    var registration = this.container.get_registration(service_type);
    return this.resolve_registration(registration, service_type);
    }
    

Update resolve_all_type

Merge results from both scope-local and container registrations:

public Enumerable<Object> resolve_all_type(Type service_type) {
    var results = new List<Object>();
    
    // Add scope-local instances first
    foreach (var registration in this.local_registrations.get_or_empty(service_type)) {
        results.append(resolve_local_registration(registration, service_type));
    }
    
    // Add container instances
    foreach (var registration in this.container.get_registrations(service_type)) {
        results.append(resolve_registration(registration, service_type));
    }
    
    return Iterate.over(results);
}

5. Child Scope Behavior

No changes needed to create_child_scope() - it already creates a fresh Scope with just a reference to the container. The new scope-local registration storage will be empty for child scopes, satisfying the isolation requirement.

Resolution Flow Diagram

flowchart TD
    A[resolve_type called] --> B{Scope has local registration?}
    B -->|Yes| C[Use scope-local factory]
    C --> D{Lifecycle?}
    D -->|Instance| E[Return cached instance]
    D -->|Transient| F[Create new via factory]
    B -->|No| G{Container has registration?}
    G -->|Yes| H[Use container factory]
    H --> I{Lifecycle?}
    I -->|Singleton| J[Return container singleton]
    I -->|Scoped| K[Return/create scoped instance]
    I -->|Transient| L[Create new via factory]
    G -->|No| M[Throw NOT_REGISTERED]

Usage Example

// In a web server handler
void handle_request(Request request) {
    // Create a scope for this request
    var scope = container.create_scope();
    
    // Register request-specific context
    var request_context = new RequestContext(request);
    scope.register_instance(typeof(RequestContext), request_context);
    
    // Register a transient factory for request-specific logger
    scope.register_transient(typeof(RequestLogger), (s) => {
        var ctx = s.resolve<RequestContext>();
        return new RequestLogger(ctx.request_id);
    });
    
    // Resolve handlers - they will receive the request_context
    var handler = scope.resolve<RequestHandler>();
    handler.handle();
}

Implementation Checklist

  • Add local_registrations Catalogue to Scope
  • Add local_factories Dictionary to Scope
  • Implement register_instance(Type, Object) on Scope
  • Implement register_transient(Type, owned FactoryDelegate) on Scope
  • Modify resolve_type to check scope-local first
  • Modify resolve_all_type to merge both sources
  • Add helper method resolve_local_registration if needed
  • Create example file demonstrating the feature
  • Add unit tests for scope-local registration behavior

Files to Modify

File Changes
src/Scope.vala Add storage fields, registration methods, update resolution logic
examples/ScopeRegistrationDemo.vala New example file demonstrating usage

Open Questions

None - requirements have been clarified.