Tutorial: Dependency Injection
Crystal.Avalonia uses Microsoft.Extensions.DependencyInjection for dependency injection. This tutorial covers DI patterns and best practices.
Basic Concepts
Constructor Injection (Recommended)
using CommunityToolkit.Mvvm.ComponentModel;
public partial class UserListViewModel : ObservableObject
{
private readonly IUserService _userService;
private readonly ILogger _logger;
// Dependencies are injected through the constructor
public UserListViewModel(IUserService userService, ILogger logger)
{
_userService = userService;
_logger = logger;
}
}
Property Injection
using CommunityToolkit.Mvvm.ComponentModel;
public partial class SettingsViewModel : ObservableObject
{
public IConfigService? ConfigService { get; set; }
}
Registering Services
In Module.RegisterServices
public class MyModule : IModule
{
public void RegisterServices(IServiceCollection services)
{
// Transient - New instance each time
services.AddTransient<IUserService, UserService>();
// Singleton - Same instance shared
services.AddSingleton<ISettingsService, SettingsService>();
// Scoped - One instance per scope
services.AddScoped<IDbContext, AppDbContext>();
// Instance - Pre-created instance
services.AddInstance(new AppMetrics());
}
}
In App.RegisterServices
public override void RegisterServices(IServiceCollection services)
{
// Register application-wide services
services.AddSingleton<IAppConfiguration, AppConfiguration>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddMvvmTransient<MainView, MainViewModel>();
}
AddMvvm* registers ViewModel in DI + View mapping. Shell: CreateShell<MainWindow, MainView>().
View/ViewModel Registration
| Method | What it does |
|---|---|
AddMvvmTransient<TView, TViewModel>() |
AddTransient<TViewModel>() + View ↔ ViewModel mapping |
AddMvvmSingleton<TView, TViewModel>() |
AddSingleton<TViewModel>() + View ↔ ViewModel mapping |
TView is not registered in DI. Views are created by XAML (View-first) or Activator.CreateInstance (ViewLocator).
Service Lifetimes
| Lifetime | When to Use | Behavior |
|---|---|---|
Transient |
Lightweight, stateless services | New instance each time |
Singleton |
Shared state, configuration | Same instance always |
Scoped |
Per-operation data (e.g., DB context) | One instance per scope |
Example: Choosing the Right Lifetime
public void RegisterServices(IServiceCollection services)
{
// Transient: New instance for each request
// Good for: ViewModels, lightweight services
services.AddTransient<MainViewModel>();
services.AddTransient<IIdGenerator, GuidIdGenerator>();
// Singleton: Shared across the app
// Good for: Configuration, logging, caching
services.AddSingleton<AppSettings>();
services.AddSingleton<ILogger, FileLogger>();
// Scoped: Per operation/scope
// Good for: Database contexts
services.AddScoped<IDbContext>(sp => new MyDbContext());
}
Advanced Patterns
Factory Pattern
public interface IViewModelFactory
{
TViewModel Create<TViewModel>() where TViewModel : class;
}
public class ViewModelFactory : IViewModelFactory
{
private readonly IServiceProvider _serviceProvider;
public ViewModelFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public TViewModel Create<TViewModel>() where TViewModel : class
{
return _serviceProvider.GetRequiredService<TViewModel>();
}
}
// Registration
services.AddSingleton<IViewModelFactory, ViewModelFactory>();
// Usage
public partial class DetailViewModel : ObservableObject
{
private readonly IViewModelFactory _factory;
public DetailViewModel(IViewModelFactory factory)
{
_factory = factory;
}
public void OpenItem(ItemViewModel item)
{
var vm = _factory.Create<ItemDetailViewModel>();
vm.Load(item);
}
}
Lazy Resolution
using CommunityToolkit.Mvvm.ComponentModel;
public partial class MyViewModel : ObservableObject
{
private readonly Lazy<IHeavyService> _heavyService;
public MyViewModel(Lazy<IHeavyService> heavyService)
{
_heavyService = heavyService;
}
public void DoWork()
{
// Service is only created when accessed
var service = _heavyService.Value;
service.Execute();
}
}
Options Pattern
// Configuration
public class AppSettings
{
public string ApiBaseUrl { get; set; } = "";
public int TimeoutSeconds { get; set; } = 30;
}
// Registration
var settings = new AppSettings
{
ApiBaseUrl = "https://api.example.com",
TimeoutSeconds = 60
};
services.AddSingleton(settings);
// Usage
public class ApiService : IApiService
{
public ApiService(AppSettings settings)
{
var baseUrl = settings.ApiBaseUrl;
var timeout = settings.TimeoutSeconds;
}
}
Cross-Module Service Sharing
Define Service in Main App
// Main App
public override void RegisterServices(IServiceCollection services)
{
services.AddSingleton<INavigationService, NavigationService>();
}
Use in Modules
public class MyModule : IModule
{
public void InitializeModule(IServiceProvider serviceProvider)
{
// NavigationService was registered in the main app
var nav = serviceProvider.GetRequiredService<INavigationService>();
nav.NavigateToHome();
}
}
Testing with DI
Register Test Doubles
public void Setup()
{
var services = new ServiceCollection();
// Register test doubles
services.AddSingleton<IUserService, MockUserService>();
services.AddSingleton<ILogger, MockLogger>();
// Register the ViewModel
services.AddTransient<UserListViewModel>();
var provider = services.BuildServiceProvider();
var vm = provider.GetRequiredService<UserListViewModel>();
}
Testing ViewModels
public class UserListViewModelTests
{
[Fact]
public void LoadUsers_UpdatesItemsList()
{
// Arrange
var mockService = new MockUserService();
var vm = new UserListViewModel(mockService);
// Act
vm.RefreshCommand.Execute();
// Assert
Assert.NotEmpty(vm.Users);
}
}
Common Pitfalls
1. Captive Dependency
// WRONG: DbContext captured by singleton
services.AddSingleton<IService>(sp =>
{
var dbContext = sp.GetRequiredService<DbContext>(); // ❌
return new Service(dbContext);
});
// CORRECT: Scoped DbContext
services.AddScoped<MyService>(sp =>
{
var dbContext = sp.GetRequiredService<DbContext>(); // ✓
return new Service(dbContext);
});
2. Circular Dependencies
// WRONG: Circular dependency
public class A
{
public A(B b) { }
}
public class B
{
public B(A a) { } // ❌ Circular!
// CORRECT: Break the cycle with an interface
public interface IA { }
public interface IB { }
public class A : IA
{
public A(IB b) { }
}
Next Steps
- Navigation - Implement navigation between views
- Module Development - Create reusable modules
Further Reading
How it works: Architecture — Bootstrap Pipeline — when App vs module services register. Design Decisions — ViewModel in DI, View via mapping only.