As .NET developers, we've all been there, adding a new service to an application and realizing that we need to jump through hoops to wire it up in the IoC container. This often involves navigating through layers of code, digging through Startup.cs or equivalent files, and explicitly adding the new service. Wouldn't it be great if there was a way to streamline this? Imagine a flag or marker that simply tells the framework, "Hey, I'm here, and I need to be registered with this scope."

Well, that's precisely what Bindicate aims to achieve.

What is Bindicate?

Bindicate is a powerful NuGet package designed to declutter your .NET configuration by enabling attribute-based service registration. By using attributes, you can dictate how your classes should be registered in the IoC container. It supports all standard service lifetimes (Scoped, Transient, Singleton), their "TryAdd" and "TryAddEnumerable" variants, keyed services for .NET 8+, decorators, and options configuration.

Key Benefits

  • Clean Code - You no longer have to comb through a mountain of services.Add...<,>() lines. Bindicate allows you to specify the registration logic at the service class level, keeping related concerns together.
  • Better Maintainability - When the registration is closely associated with the service class, it's easier to change the scope of the service without hunting down the registration code in a different part of the project.
  • Comprehensive Feature Set - Bindicate supports advanced scenarios including decorator patterns, multiple implementations, generic interfaces, and configuration options.

Feature Matrix

Feature Regular Keyed (.NET 8) TryAdd TryAddEnumerable Decorators
AddTransient ? ? ? ? ?
AddScoped ? ? ? ? ?
AddSingleton ? ? ? ? ?

Getting Started

Installation

To get started, install the package from NuGet:

Install-Package Bindicate

or

dotnet add package Bindicate

Basic Service Registration

Once installed, you can start decorating your service classes with Bindicate attributes:

[AddScoped]
public class UserRepository : IUserRepository
{
    private readonly DbContext _context;

    public UserRepository(DbContext context)
    {
        _context = context;
    }

    public async Task<User> GetByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }
}

public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);
}

Or specify the interface explicitly:

[AddTransient(typeof(IEmailService))]
public class EmailService : IEmailService
{
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        // Implementation
    }
}

Bootstrapping

After your services are appropriately decorated, initialize Bindicate in your Program.cs:

.NET 6+ in Program.cs

builder.Services
    .AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
    .Register();

For older versions in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
        .Register();
    
    // Other configurations
}
Important: Don't forget to call .Register() at the end!

Advanced Features

Keyed Services (.NET 8+)

Keyed services allow you to register multiple implementations of the same interface, identified by a unique key:

public enum PaymentProvider
{
    Stripe,
    PayPal,
    Square
}

[AddKeyedScoped(PaymentProvider.Stripe, typeof(IPaymentProcessor))]
public class StripePaymentProcessor : IPaymentProcessor
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        // Stripe implementation
    }
}

[AddKeyedScoped(PaymentProvider.PayPal, typeof(IPaymentProcessor))]
public class PayPalPaymentProcessor : IPaymentProcessor
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        // PayPal implementation
    }
}

Usage in your service:

public class OrderService
{
    private readonly IPaymentProcessor _stripeProcessor;

    public OrderService([FromKeyedServices(PaymentProvider.Stripe)] IPaymentProcessor processor)
    {
        _stripeProcessor = processor;
    }
}

Bootstrapping Keyed Services

Add the ForKeyedServices() method call to your configuration:

builder.Services
    .AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
    .ForKeyedServices()
    .Register();

Multiple Service Implementations (TryAddEnumerable)

When you need multiple implementations of the same interface available simultaneously:

public interface INotificationHandler
{
    Task HandleAsync(string message);
}

[TryAddEnumerable(Lifetime.TryAddEnumerableTransient, typeof(INotificationHandler))]
public class EmailNotificationHandler : INotificationHandler
{
    public async Task HandleAsync(string message)
    {
        // Send email notification
    }
}

[TryAddEnumerable(Lifetime.TryAddEnumerableTransient, typeof(INotificationHandler))]
public class SmsNotificationHandler : INotificationHandler
{
    public async Task HandleAsync(string message)
    {
        // Send SMS notification
    }
}

All implementations available via IEnumerable<INotificationHandler>:

public class NotificationService
{
    private readonly IEnumerable<INotificationHandler> _handlers;

    public NotificationService(IEnumerable<INotificationHandler> handlers)
    {
        _handlers = handlers;
    }

    public async Task NotifyAllAsync(string message)
    {
        await Task.WhenAll(_handlers.Select(h => h.HandleAsync(message)));
    }
}

Decorator Pattern

Bindicate supports the decorator pattern, allowing you to wrap services with additional behavior like logging, caching, or validation:

public interface IOrderService
{
    Task<Order> CreateOrderAsync(CreateOrderRequest request);
}

[AddScoped(typeof(IOrderService))]
public class OrderService : IOrderService
{
    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // Core business logic
        return new Order { Id = Guid.NewGuid(), Total = request.Total };
    }
}

[RegisterDecorator(typeof(IOrderService), order: 1)]
public class LoggingOrderDecorator : IOrderService
{
    private readonly IOrderService _inner;
    private readonly ILogger<LoggingOrderDecorator> _logger;

    public LoggingOrderDecorator(IOrderService inner, ILogger<LoggingOrderDecorator> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        _logger.LogInformation("Creating order for amount: {Amount}", request.Total);
        var order = await _inner.CreateOrderAsync(request);
        _logger.LogInformation("Order created with ID: {OrderId}", order.Id);
        return order;
    }
}

[RegisterDecorator(typeof(IOrderService), order: 2)]
public class CachingOrderDecorator : IOrderService
{
    private readonly IOrderService _inner;
    private readonly IMemoryCache _cache;

    public CachingOrderDecorator(IOrderService inner, IMemoryCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // Caching logic would go here
        return await _inner.CreateOrderAsync(request);
    }
}

Decorators are applied automatically when you call Register(). The order parameter controls the application sequence (lower values are applied first).

Generic Interface Support

Bindicate can automatically register generic interfaces:

[RegisterGenericInterface]
public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
}

[AddScoped(typeof(IRepository<>))]
public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;

    public Repository(DbContext context)
    {
        _context = context;
    }

    public async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }

    public async Task<IEnumerable<T>> GetAllAsync()
    {
        return await _context.Set<T>().ToListAsync();
    }
}

Options Configuration

Register and configure options classes directly from appsettings.json:

[RegisterOptions("EmailSettings")]
public class EmailOptions
{
    public string SmtpServer { get; set; } = "";
    public int Port { get; set; }
    public string Username { get; set; } = "";
    public string Password { get; set; } = "";
}

appsettings.json:

{
  "EmailSettings": {
    "SmtpServer": "smtp.gmail.com",
    "Port": 587,
    "Username": "your-email@gmail.com",
    "Password": "your-password"
  }
}

Then update your service registration to include options:

builder.Services
    .AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
    .WithOptions(builder.Configuration)  // Pass configuration here
    .Register();

Multi-Assembly Support

For larger applications with multiple assemblies:

builder.Services
    .AddAutowiringForAssembly(typeof(DataService).Assembly)      // Data layer
    .AddAutowiringForAssembly(typeof(BusinessService).Assembly)  // Business layer
    .AddAutowiringForAssembly(typeof(ApiController).Assembly)    // API layer
    .ForKeyedServices()
    .WithOptions(builder.Configuration)
    .Register();

Best Practices

  1. Performance: Only scan assemblies that contain your services to improve startup performance
  2. Decorator Ordering: Use the order parameter thoughtfully to control decorator application sequence
  3. Keyed Services: Use meaningful keys (enums or constants) rather than magic strings
  4. Options Validation: Combine with IValidateOptions<T> for robust configuration validation
  5. Assembly Organization: Group related services in the same assembly for easier management

Troubleshooting

Common Issues

  • Services not found: Ensure you're scanning the correct assembly containing your services
  • Circular dependencies: Consider using factory patterns or lazy initialization
  • Missing Register() call: Don't forget to call .Register() at the end of your fluent chain
  • Keyed service not resolved: Make sure you've called .ForKeyedServices() in your configuration
  • Decorator not applied: Ensure the original service is registered before applying decorators

Debugging Tips

Check what services are registered:

var services = new ServiceCollection();
services.AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
    .Register();

// Inspect the registered services
foreach (var service in services)
{
    Console.WriteLine($"{service.ServiceType.Name} -> {service.ImplementationType?.Name ?? "Factory"}");
}

Current Limitations

  • Single Registration Policy: One primary registration attribute per class (decorators are separate)
  • Assembly Scanning: Requires explicit assembly specification for each library
  • Keyed Services: TryAdd variants not yet supported for keyed services
  • .NET 8 Requirement: Keyed services require .NET 8 or later

Conclusion

Bindicate transforms dependency injection in .NET applications from a configuration chore into an elegant, declarative system. By moving service registration closer to the service definitions themselves, it promotes cleaner, more maintainable code while supporting advanced scenarios like decorators, keyed services, and multiple implementations.

Whether you're building a simple web API or a complex enterprise application, Bindicate's attribute-based approach will streamline your dependency injection setup and make your codebase more readable and maintainable.

The library continues to evolve with new features and improvements, making it an excellent choice for modern .NET applications targeting .NET 8 and beyond.

Resources

Comments