Table of Contents

Dependency Injection

The Cratis.DependencyInjection namespace provides convention-based dependency injection features that simplify service registration in .NET applications. These utilities help reduce boilerplate code when working with Microsoft's dependency injection container.

Overview

The dependency injection utilities offer:

  • Convention-based registration: Automatically register services following naming conventions
  • Self-binding: Register concrete types for direct injection
  • Lifecycle management: Control service lifetimes using attributes
  • Selective registration: Exclude types from convention-based registration

Extension Methods

AddBindingsByConvention

Automatically registers services based on interface naming conventions.

services.AddBindingsByConvention();

Convention Rules:

  • Interface and implementation must be in the same namespace
  • Interface name must be I{ImplementationName} (e.g., IUserServiceUserService)
  • Only one implementation per interface is allowed
  • Types with [IgnoreConvention] attribute are excluded

Examples:

// These will be automatically registered:
public interface IUserService { }
public class UserService : IUserService { } // Registered as Transient

public interface IOrderProcessor { }
[Singleton]
public class OrderProcessor : IOrderProcessor { } // Registered as Singleton

// This will be ignored:
[IgnoreConvention]
public class TestUserService : IUserService { }

AddSelfBindings

Registers concrete types for direct injection without requiring an interface.

services.AddSelfBindings();

Registration Rules:

  • Non-static, non-abstract, non-interface types
  • Types not derived from Exception
  • Types without [IgnoreConvention] attribute
  • Types not in System or Microsoft namespaces
  • Types with resolvable constructor parameters

Examples:

// These will be automatically registered:
public class EmailSender { } // Registered as Transient

[Singleton]
public class CacheManager { } // Registered as Singleton

// These will be ignored:
public class SystemType { } // In System namespace
[IgnoreConvention]
public class InternalService { } // Has ignore attribute
public class BadService(string connectionString) { } // Primitive parameter

Attributes

SingletonAttribute

Marks a class to be registered as a singleton service.

[Singleton]
public class DatabaseConnection : IDatabaseConnection
{
    // This service will be registered as a singleton
}

Usage:

  • Apply to class declarations
  • Works with both convention-based and self-binding registration
  • Overrides the default transient lifetime

IgnoreConventionAttribute

Excludes a class from convention-based registration.

[IgnoreConvention]
public class TestImplementation : IService
{
    // This class will not be automatically registered
}

Usage:

  • Apply to class declarations
  • Useful for test implementations, decorators, or manually registered services
  • Affects both AddBindingsByConvention and AddSelfBindings

Best Practices

1. Use Convention-Based Registration First

Start with convention-based registration for most services:

public void ConfigureServices(IServiceCollection services)
{
    services.AddBindingsByConvention();
    services.AddSelfBindings();
    
    // Manual registrations for special cases
    services.AddScoped<ISpecialService, CustomImplementation>();
}

2. Follow Naming Conventions

Ensure your interfaces and implementations follow the expected patterns:

// ✅ Good - follows convention
public interface IUserRepository { }
public class UserRepository : IUserRepository { }

// ❌ Bad - doesn't follow convention
public interface UserRepo { }
public class UserRepositoryImpl : UserRepo { }

3. Use Attributes Appropriately

Apply lifecycle and exclusion attributes where needed:

// Singleton for expensive-to-create services
[Singleton]
public class DatabaseConnectionPool : IDatabaseConnectionPool { }

// Ignore for test doubles or special implementations
[IgnoreConvention]
public class MockUserService : IUserService { }

4. Avoid Primitive Dependencies

Constructor parameters should use concepts or dependency injection:

// ❌ Bad - primitive parameters prevent auto-registration
public class EmailService(string smtpServer, int port) { }

// ✅ Good - uses concepts and injection
public class EmailService(SmtpConfiguration config, ILogger<EmailService> logger) { }

Integration Example

Complete setup in a typical ASP.NET Core application:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Add convention-based services
        services.AddBindingsByConvention();
        services.AddSelfBindings();
        
        // Add framework services
        services.AddControllers();
        services.AddDbContext<MyDbContext>(options => 
            options.UseSqlServer(connectionString));
        
        // Manual registrations for complex scenarios
        services.AddScoped<IEmailService>(provider => 
            new EmailService(provider.GetRequiredService<IConfiguration>()));
    }
}

This approach significantly reduces the amount of manual service registration code while maintaining flexibility for special cases.