Skip to content

Decorator support for registered services #7

@dmytroett

Description

@dmytroett

Description

Add support for the Decorator pattern, allowing services to be wrapped with decorators automatically during registration. This enables cross-cutting concerns like logging, caching, validation, and performance monitoring without modifying the original service implementation.

Motivation

The Decorator pattern is a powerful way to add behavior to services dynamically. Currently, decorators must be manually configured in DI setup code. With AttributedDI, decorators should be discoverable and registered automatically via attributes.

Usage Scenario

Basic Decorator Registration

public interface IUserService
{
    User GetUser(int id);
    void UpdateUser(User user);
}

[RegisterAs<IUserService>]
[Singleton]
public class UserService : IUserService
{
    public User GetUser(int id) => new User { Id = id, Name = "John" };
    public void UpdateUser(User user) { }
}

// Register a logging decorator
[RegisterAsDecorator<IUserService>]
[Singleton]
public class LoggingUserServiceDecorator : IUserService
{
    private readonly IUserService _inner;

    public LoggingUserServiceDecorator(IUserService inner)
    {
        _inner = inner;
    }

    public User GetUser(int id)
    {
        Console.WriteLine($"Getting user {id}");
        return _inner.GetUser(id);
    }

    public void UpdateUser(User user)
    {
        Console.WriteLine($"Updating user {user.Id}");
        _inner.UpdateUser(user);
    }
}

// Register a caching decorator
[RegisterAsDecorator<IUserService>]
[Scoped]
public class CachingUserServiceDecorator : IUserService
{
    private readonly IUserService _inner;
    private readonly Dictionary<int, User> _cache = new();

    public CachingUserServiceDecorator(IUserService inner)
    {
        _inner = inner;
    }

    public User GetUser(int id)
    {
        if (_cache.TryGetValue(id, out var user))
            return user;
        
        user = _inner.GetUser(id);
        _cache[id] = user;
        return user;
    }

    public void UpdateUser(User user)
    {
        _inner.UpdateUser(user);
        _cache[user.Id] = user;
    }
}

When AddServices() is called:

  1. The core UserService is registered as IUserService
  2. A decorator composition is created: CachingUserServiceDecorator wrapping LoggingUserServiceDecorator wrapping UserService
  3. When IUserService is injected, the outermost decorator is provided

Decorator Ordering

The first decorator registered becomes the outermost decorator:

CachingUserServiceDecorator
  -> LoggingUserServiceDecorator
    -> UserService

This means caching wraps logging wraps the actual service.

Implementation Notes

  • Add RegisterAsDecoratorAttribute<TService> attribute
  • Decorators must have a constructor accepting TService (or Lazy<TService> for deferred initialization)
  • Support all lifetime attributes with decorators
  • Handle keyed services: decorators can decorate keyed services with the same key
  • Validate that decorator types implement/inherit from the service type
  • Decorators are applied in the order they are declared (first = outermost)
  • Add comprehensive unit tests for decorator chain composition
  • Add integration tests covering multiple decorators, mixed lifetimes, and keyed services
  • Update documentation with decorator examples and best practices

Validation Rules

  • A decorator must have exactly one constructor
  • That constructor must accept a parameter of type TService (the interface being decorated)
  • Decorator classes should not be registered as regular services (validate in source generator)
  • Decorators and the core service should have compatible lifetime assignments

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions