| name | myth-dependency-injection |
| description | Use when you need automatic service registration via assembly scanning. TypeProvider gives access to all application assemblies and types. AddServiceFromType<T>() auto-registers implementations by naming convention (IUserRepository → UserRepository). Supports Scoped, Transient, and Singleton lifetimes. |
SKILL.md - Myth.DependencyInjection
Version: 1.0
Target Framework: .NET 8.0
License: Apache 2.0
Table of Contents
Overview
Myth.DependencyInjection provides powerful tools for assembly scanning, type discovery, and automatic service registration in .NET applications. It simplifies dependency injection setup by automatically discovering and registering implementations based on conventions.
Key Features
- Assembly Discovery: Automatic discovery of all application assemblies
- Type Scanning: Scan and filter types across the entire application
- Auto-Registration: Convention-based automatic service registration
- Interface Discovery: Find implementations by interface or base class
- Flexible Lifetimes: Support for Scoped, Transient, and Singleton lifetimes
Installation
dotnet add package Myth.DependencyInjection
Dependencies
- .NET 8.0 or higher
- Microsoft.Extensions.DependencyInjection.Abstractions
- Myth.Commons
Core Concepts
1. Assembly Scanning
The library automatically scans all application assemblies, including:
- Already loaded assemblies in
AppDomain.CurrentDomain
- DLL files in the application base directory
- Excludes dynamic assemblies
2. Type Discovery
Provides filtered access to all concrete types in the application:
- Non-abstract types only
- Non-interface types only
- Supports filtering by base class or interface
3. Convention-Based Registration
Automatically registers services using naming conventions:
- Interface:
IUserRepository → Implementation: UserRepository ✅
- Interface:
IOrderService → Implementation: OrderService ✅
- Interface name must be contained in implementation name
API Reference
TypeProvider
Namespace: Myth.ValueProviders
Static class providing access to application assemblies and types.
Properties
public static class TypeProvider {
public static string? BaseApplicationNamespace { get; }
public static IEnumerable<Assembly> ApplicationAssemblies { get; }
public static IEnumerable<Type> ApplicationTypes { get; }
}
Examples:
var baseNamespace = TypeProvider.BaseApplicationNamespace;
var assemblies = TypeProvider.ApplicationAssemblies;
foreach (var assembly in assemblies) {
Console.WriteLine(assembly.GetName().Name);
}
var types = TypeProvider.ApplicationTypes;
var classCount = types.Count();
Console.WriteLine($"Found {classCount} concrete types");
Methods
public static IEnumerable<Type> GetTypesAssignableFrom<TType>()
Example:
var repositoryTypes = TypeProvider.GetTypesAssignableFrom<IRepository>();
foreach (var type in repositoryTypes) {
Console.WriteLine($"Found repository: {type.Name}");
}
var handlerTypes = TypeProvider.GetTypesAssignableFrom<ICommandHandler>();
var serviceTypes = TypeProvider.GetTypesAssignableFrom<IDomainService>();
Auto-Registration
Namespace: Myth.Extensions
Extension methods for automatic service registration.
AddServiceFromType
public static IServiceCollection AddServiceFromType<TType>(
this IServiceCollection services,
ServiceLifetime serviceLifetime = ServiceLifetime.Scoped
)
Parameters:
services: Service collection to register implementations
TType: Base interface or class to find implementations
serviceLifetime: Service lifetime (default: Scoped)
Returns: The service collection for chaining
Behavior:
- Finds all types implementing
TType using TypeProvider.GetTypesAssignableFrom<TType>()
- For each implementation:
- Searches for interface whose name contains the implementation name
- Registers interface → implementation with specified lifetime
- Throws
InterfaceNotFoundException if no matching interface found
Naming Convention:
- Interface name must contain the implementation class name (without "I" prefix)
- Case-sensitive matching
Examples:
services.AddServiceFromType<IRepository>();
services.AddServiceFromType<ICommandHandler>(ServiceLifetime.Transient);
services.AddServiceFromType<IDomainService>(ServiceLifetime.Singleton);
Valid Naming Patterns:
| Interface | Implementation | Match? |
|---|
IUserRepository | UserRepository | ✅ Yes |
IOrderService | OrderService | ✅ Yes |
IProductRepository | ProductRepositoryImpl | ✅ Yes (contains "ProductRepository") |
ICustomerService | CustomerServiceImplementation | ✅ Yes (contains "CustomerService") |
IUserRepository | UserDataAccess | ❌ No (doesn't contain "UserRepository") |
IOrderRepository | OrderDao | ❌ No (doesn't contain "OrderRepository") |
Exceptions
InterfaceNotFoundException
Namespace: Myth.Exceptions
public class InterfaceNotFoundException : Exception {
public InterfaceNotFoundException(string? message)
}
When Thrown:
- During
AddServiceFromType<T>() when an implementation doesn't have a matching interface
- Message format: "Not found a interface that corresponds to type {TypeName}"
Example:
services.AddServiceFromType<IRepository>();
Solution:
Ensure implementation names follow convention:
public interface IUserRepository : IRepository { }
public class UserDataAccess : IUserRepository { }
public interface IUserRepository : IRepository { }
public class UserRepository : IUserRepository { }
Usage Examples
Example 1: Basic Repository Auto-Registration
public interface IRepository { }
public interface IUserRepository : IRepository {
Task<User?> GetByIdAsync(Guid id);
Task<IEnumerable<User>> GetAllAsync();
}
public interface IOrderRepository : IRepository {
Task<Order?> GetByIdAsync(Guid id);
Task<IEnumerable<Order>> GetByUserIdAsync(Guid userId);
}
public class UserRepository : IUserRepository {
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context) {
_context = context;
}
public async Task<User?> GetByIdAsync(Guid id) {
return await _context.Users.FindAsync(id);
}
public async Task<IEnumerable<User>> GetAllAsync() {
return await _context.Users.ToListAsync();
}
}
public class OrderRepository : IOrderRepository {
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context) {
_context = context;
}
public async Task<Order?> GetByIdAsync(Guid id) {
return await _context.Orders.FindAsync(id);
}
public async Task<IEnumerable<Order>> GetByUserIdAsync(Guid userId) {
return await _context.Orders.Where(o => o.UserId == userId).ToListAsync();
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceFromType<IRepository>();
var app = builder.BuildApp();
app.Run();
Example 2: CQRS Pattern with Auto-Registration
public interface ICommandHandler { }
public interface ICommandHandler<TCommand> : ICommandHandler {
Task HandleAsync(TCommand command, CancellationToken cancellationToken);
}
public interface IQueryHandler { }
public interface IQueryHandler<TQuery, TResult> : IQueryHandler {
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken);
}
public class CreateUserCommand {
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand> {
private readonly IUserRepository _repository;
public CreateUserCommandHandler(IUserRepository repository) {
_repository = repository;
}
public async Task HandleAsync(CreateUserCommand command, CancellationToken ct) {
var user = new User {
Name = command.Name,
Email = command.Email
};
await _repository.AddAsync(user, ct);
}
}
public class GetUserByIdQuery {
public Guid UserId { get; set; }
}
public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, User?> {
private readonly IUserRepository _repository;
public GetUserByIdQueryHandler(IUserRepository repository) {
_repository = repository;
}
public async Task<User?> HandleAsync(GetUserByIdQuery query, CancellationToken ct) {
return await _repository.GetByIdAsync(query.UserId);
}
}
builder.Services.AddServiceFromType<ICommandHandler>(ServiceLifetime.Transient);
builder.Services.AddServiceFromType<IQueryHandler>(ServiceLifetime.Transient);
Example 3: Domain-Driven Design Layers
public interface IInfrastructureService { }
public interface IDomainService { }
public interface IApplicationService { }
public interface IEmailService : IInfrastructureService {
Task SendEmailAsync(string to, string subject, string body);
}
public class EmailService : IEmailService {
public async Task SendEmailAsync(string to, string subject, string body) {
}
}
public interface IOrderDomainService : IDomainService {
decimal CalculateTotal(Order order);
bool CanBeCancelled(Order order);
}
public class OrderDomainService : IOrderDomainService {
public decimal CalculateTotal(Order order) {
return order.Items.Sum(i => i.Price * i.Quantity);
}
public bool CanBeCancelled(Order order) {
return order.Status == OrderStatus.Pending ||
order.Status == OrderStatus.Confirmed;
}
}
public interface IOrderApplicationService : IApplicationService {
Task<OrderDto> CreateOrderAsync(CreateOrderCommand command);
}
public class OrderApplicationService : IOrderApplicationService {
private readonly IOrderRepository _repository;
private readonly IOrderDomainService _domainService;
public OrderApplicationService(
IOrderRepository repository,
IOrderDomainService domainService) {
_repository = repository;
_domainService = domainService;
}
public async Task<OrderDto> CreateOrderAsync(CreateOrderCommand command) {
var order = new Order { };
await _repository.AddAsync(order);
return order.To<OrderDto>();
}
}
builder.Services.AddServiceFromType<IInfrastructureService>();
builder.Services.AddServiceFromType<IDomainService>();
builder.Services.AddServiceFromType<IApplicationService>();
Example 4: Plugin Architecture
public interface IPlugin {
string Name { get; }
string Version { get; }
void Initialize();
}
public class PaymentPlugin : IPlugin {
public string Name => "Payment Processor";
public string Version => "1.0.0";
public void Initialize() {
Console.WriteLine("Payment plugin initialized");
}
}
public class NotificationPlugin : IPlugin {
public string Name => "Notification Service";
public string Version => "2.1.0";
public void Initialize() {
Console.WriteLine("Notification plugin initialized");
}
}
public class PluginLoader {
public void LoadPlugins() {
var pluginTypes = TypeProvider.GetTypesAssignableFrom<IPlugin>();
Console.WriteLine($"Found {pluginTypes.Count()} plugins:");
foreach (var pluginType in pluginTypes) {
var plugin = (IPlugin)Activator.CreateInstance(pluginType)!;
Console.WriteLine($" - {plugin.Name} v{plugin.Version}");
plugin.Initialize();
}
}
}
var loader = new PluginLoader();
loader.LoadPlugins();
Example 5: Assembly Analysis and Diagnostics
public class AssemblyAnalyzer {
public void PrintApplicationStructure() {
Console.WriteLine("=== Application Structure ===\n");
Console.WriteLine($"Base Namespace: {TypeProvider.BaseApplicationNamespace}\n");
Console.WriteLine("Assemblies:");
foreach (var assembly in TypeProvider.ApplicationAssemblies) {
var name = assembly.GetName();
Console.WriteLine($" - {name.Name} v{name.Version}");
}
var allTypes = TypeProvider.ApplicationTypes;
Console.WriteLine($"\nTotal Types: {allTypes.Count()}");
var repositories = TypeProvider.GetTypesAssignableFrom<IRepository>();
Console.WriteLine($"\nRepositories ({repositories.Count()}):");
foreach (var repo in repositories) {
Console.WriteLine($" - {repo.Name}");
}
var services = TypeProvider.GetTypesAssignableFrom<IService>();
Console.WriteLine($"\nServices ({services.Count()}):");
foreach (var service in services) {
Console.WriteLine($" - {service.Name}");
}
var handlers = TypeProvider.GetTypesAssignableFrom<IHandler>();
Console.WriteLine($"\nHandlers ({handlers.Count()}):");
foreach (var handler in handlers) {
Console.WriteLine($" - {handler.Name}");
}
}
public void ValidateRegistrations() {
var services = new ServiceCollection();
services.AddServiceFromType<IRepository>();
var provider = services.BuildServiceProvider();
var requiredRepositories = new[] {
typeof(IUserRepository),
typeof(IOrderRepository),
typeof(IProductRepository)
};
foreach (var repoType in requiredRepositories) {
var service = provider.GetService(repoType);
if (service == null) {
throw new InvalidOperationException(
$"Required service {repoType.Name} is not registered!");
}
Console.WriteLine($"✓ {repoType.Name} is registered");
}
}
}
var analyzer = new AssemblyAnalyzer();
analyzer.PrintApplicationStructure();
analyzer.ValidateRegistrations();
Advanced Patterns
Pattern 1: Marker Interfaces with Shared Behavior
public interface IDomainService { }
public interface IOrderService : IDomainService {
Task<Order> CreateOrderAsync(CreateOrderCommand command);
}
public interface IProductService : IDomainService {
Task<Product> GetProductAsync(Guid id);
}
public class OrderService : IOrderService {
public async Task<Order> CreateOrderAsync(CreateOrderCommand command) {
}
}
public class ProductService : IProductService {
public async Task<Product> GetProductAsync(Guid id) {
}
}
services.AddServiceFromType<IDomainService>();
Pattern 2: Mixed Auto-Registration and Manual Registration
services.AddServiceFromType<IRepository>();
services.AddServiceFromType<IDomainService>();
services.AddSingleton<IConfiguration>(configuration);
services.AddScoped<ICurrentUser, HttpContextCurrentUserAccessor>();
services.AddTransient<IEmailSender, SendGridEmailSender>();
services.AddScoped<ISpecialRepository, CustomSpecialRepository>();
Pattern 3: Conditional Registration
var services = new ServiceCollection();
services.AddServiceFromType<IRepository>();
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IUserRepository));
if (descriptor == null) {
services.AddScoped<IUserRepository, FallbackUserRepository>();
}
Pattern 4: Multi-Interface Implementations
When a class implements multiple interfaces, auto-registration picks the first interface whose name contains the class name.
public interface IUserRepository : IRepository { }
public interface IReadOnlyUserRepository : IRepository { }
public class UserRepository : IUserRepository, IReadOnlyUserRepository {
}
services.AddServiceFromType<IRepository>();
services.AddScoped<IReadOnlyUserRepository, UserRepository>();
Pattern 5: Generic Type Discovery
public interface IValidator<T> {
Task<bool> ValidateAsync(T entity);
}
public class UserValidator : IValidator<User> {
public async Task<bool> ValidateAsync(User entity) {
return !string.IsNullOrEmpty(entity.Name);
}
}
var validatorTypes = TypeProvider.ApplicationTypes
.Where(t => t.GetInterfaces()
.Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IValidator<>)));
foreach (var validatorType in validatorTypes) {
var interfaceType = validatorType.GetInterfaces()
.First(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IValidator<>));
services.AddScoped(interfaceType, validatorType);
Console.WriteLine($"Registered {interfaceType.Name} → {validatorType.Name}");
}
Best Practices
1. Naming Conventions
✅ DO:
- Use consistent naming:
I{Name}Repository → {Name}Repository
- Keep interface and implementation names aligned
- Use descriptive names that indicate purpose
public interface IUserRepository : IRepository { }
public class UserRepository : IUserRepository { }
public interface IOrderService : IDomainService { }
public class OrderService : IOrderService { }
❌ DON'T:
- Use arbitrary implementation names
- Mix naming patterns
public interface IUserRepository : IRepository { }
public class UserDataAccess : IUserRepository { }
public interface IOrderRepository : IRepository { }
public class OrderDao : IOrderRepository { }
2. Marker Interfaces
✅ DO:
- Use marker interfaces to group related services
- Keep marker interfaces empty
- Use meaningful names
public interface IRepository { }
public interface IDomainService { }
public interface IApplicationService { }
❌ DON'T:
- Add methods to marker interfaces (defeats the purpose)
- Use generic names like
IService (too broad)
3. Service Lifetimes
Choose appropriate lifetimes:
services.AddServiceFromType<IRepository>(ServiceLifetime.Scoped);
services.AddServiceFromType<ICommandHandler>(ServiceLifetime.Transient);
services.AddServiceFromType<IValidator>(ServiceLifetime.Transient);
services.AddServiceFromType<ICacheService>(ServiceLifetime.Singleton);
4. Validation
Validate registrations at startup:
var app = builder.Build();
if (app.Environment.IsDevelopment()) {
using var scope = app.Services.CreateScope();
var provider = scope.ServiceProvider;
var userRepo = provider.GetRequiredService<IUserRepository>();
var orderService = provider.GetRequiredService<IOrderService>();
Console.WriteLine("✓ All critical services registered successfully");
}
app.Run();
5. Assembly Loading
Be aware of assembly loading:
Assembly.LoadFrom("MyPlugin.dll");
var pluginTypes = TypeProvider.GetTypesAssignableFrom<IPlugin>();
Troubleshooting
Issue 1: InterfaceNotFoundException
Problem:
InterfaceNotFoundException: Not found a interface that corresponds to type UserDataAccess
Cause: Implementation class name doesn't contain interface name.
Solution:
public interface IUserRepository : IRepository { }
public class UserDataAccess : IUserRepository { }
public interface IUserRepository : IRepository { }
public class UserRepository : IUserRepository { }
Issue 2: Services Not Discovered
Problem: Some types are not being registered.
Possible Causes:
-
Type is abstract or interface
public abstract class BaseRepository : IRepository { }
public interface IRepository { }
-
Assembly not loaded
Assembly.LoadFrom("MyAssembly.dll");
-
Type not public
internal class UserRepository : IUserRepository { }
public class UserRepository : IUserRepository { }
Issue 3: Multiple Interface Matches
Problem: Class implements multiple interfaces, wrong one gets registered.
Cause: Auto-registration picks first matching interface.
Solution:
services.AddServiceFromType<IRepository>();
services.AddScoped<IReadOnlyUserRepository, UserRepository>();
services.AddScoped<ICachedUserRepository, UserRepository>();
Issue 4: Service Already Registered
Problem: Service gets registered multiple times.
Solution: Check before auto-registering:
if (!services.Any(d => d.ServiceType == typeof(IUserRepository))) {
services.AddServiceFromType<IRepository>();
}
Issue 5: Performance with Large Codebases
Problem: Assembly scanning is slow in large applications.
Solutions:
-
Cache assemblies:
private static IEnumerable<Assembly>? _cachedAssemblies;
public static IEnumerable<Assembly> GetAssemblies() {
return _cachedAssemblies ??= TypeProvider.ApplicationAssemblies.ToList();
}
-
Use manual registration for critical paths:
services.AddServiceFromType<IBackgroundService>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IAuthService, AuthService>();
Integration with Myth Ecosystem
With Myth.Commons
using Myth.Extensions;
using Myth.ValueProviders;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceFromType<IRepository>();
var app = builder.BuildApp();
app.Run();
With Myth.Flow.Actions
builder.Services.AddServiceFromType<ICommandHandler>(ServiceLifetime.Scoped);
builder.Services.AddServiceFromType<IQueryHandler>(ServiceLifetime.Scoped);
builder.Services.AddServiceFromType<IEventHandler>(ServiceLifetime.Scoped);
builder.Services.AddFlowActions(config => {
config.UseInMemory();
config.UseCaching();
});
Performance Considerations
- Assembly Scanning: Happens once per property access (no persistent cache)
- Type Discovery: Uses LINQ for efficient filtering
- Registration Time: Auto-registration happens at startup (one-time cost)
- Runtime Performance: No overhead after registration (standard DI resolution)
Testing
Unit Test Example
using Xunit;
using FluentAssertions;
using Myth.Extensions;
using Myth.ValueProviders;
public class TypeProviderTests {
[Fact]
public void BaseApplicationNamespace_ShouldReturnFirstPart() {
var baseNamespace = TypeProvider.BaseApplicationNamespace;
baseNamespace.Should().NotBeNullOrEmpty();
baseNamespace.Should().Be("Myth");
}
[Fact]
public void ApplicationAssemblies_ShouldReturnAssemblies() {
var assemblies = TypeProvider.ApplicationAssemblies;
assemblies.Should().NotBeEmpty();
}
[Fact]
public void GetTypesAssignableFrom_ShouldReturnImplementations() {
var types = TypeProvider.GetTypesAssignableFrom<IRepository>();
types.Should().NotBeEmpty();
types.Should().AllSatisfy(t => typeof(IRepository).IsAssignableFrom(t));
}
}
public class ServiceCollectionExtensionsTests {
[Fact]
public void AddServiceFromType_ShouldRegisterServices() {
var services = new ServiceCollection();
services.AddServiceFromType<IRepository>();
var descriptor = services.FirstOrDefault(
d => d.ServiceType == typeof(IUserRepository));
descriptor.Should().NotBeNull();
descriptor!.Lifetime.Should().Be(ServiceLifetime.Scoped);
descriptor.ImplementationType.Should().Be(typeof(UserRepository));
}
[Fact]
public void AddServiceFromType_WithCustomLifetime_ShouldUseSpecifiedLifetime() {
var services = new ServiceCollection();
services.AddServiceFromType<ICommandHandler>(ServiceLifetime.Transient);
var descriptor = services.First();
descriptor.Lifetime.Should().Be(ServiceLifetime.Transient);
}
}
Summary
Myth.DependencyInjection provides powerful tools for:
- ✅ Assembly Discovery: Automatic scanning of all application assemblies
- ✅ Type Scanning: Filter and find types by base class or interface
- ✅ Auto-Registration: Convention-based automatic service registration
- ✅ Flexible Lifetimes: Support for Scoped, Transient, and Singleton
- ✅ Integration: Seamless integration with Myth ecosystem
Key Benefits:
- Reduces Boilerplate: One line replaces dozens of manual registrations
- Prevents Errors: Convention-based registration reduces typos
- Improves Maintainability: New services auto-register when following conventions
- Supports Scaling: Easily handle hundreds of services
- Type Safety: Compile-time checking with generics
Additional Resources
This documentation is maintained for AI agents and developers. For questions or contributions, please refer to the repository.