Frissítve: 2026. május 30.
A .NET MAUI Dependency Injection (DI) egy beépített tervezési minta, amely a Microsoft.Extensions.DependencyInjection konténerrel kezeli az alkalmazás osztályainak függőségeit a MauiProgram.cs fájlban regisztrált szolgáltatásokon keresztül. Ez teszi lehetővé, hogy a ViewModelek, oldalak és háttérszolgáltatások (HttpClient, naplózás, beállítások) tiszta konstruktor-injektálással kapják meg, amire szükségük van, anélkül, hogy a kódunk merev statikus hivatkozásokkal csatolódna össze. Ebben az útmutatóban végigvesszük a regisztrálást, az élettartamokat, a Shell-integrációt és a teszthelyzeteket gyakorlati .NET 10 példákkal.
Mi az a Dependency Injection a .NET MAUI-ban?
A Dependency Injection (DI) egy inversion-of-control technika, ahol egy objektum a függőségeit (más szolgáltatásokat) nem maga példányosítja, hanem kívülről kapja meg, leggyakrabban a konstruktorán keresztül. A .NET MAUI az első verziójától kezdve a Microsoft.Extensions.DependencyInjection NuGet csomagot használja konténerként, ami pontosan ugyanaz az implementáció, mint az ASP.NET Core mögött. Ez azt jelenti, hogy ha valaha is dolgoztál ASP.NET Core API-val, a MAUI DI mentális modellje azonnal ismerős lesz.
A gyakorlatban ez a következőképpen néz ki: minden funkciónk, ami egy ViewModelben, oldalon vagy háttérosztályban él, regisztrált szolgáltatásként megkaphatja az alkalmazás többi részét. Egy UserProfileViewModel nem hozza létre saját maga az HttpClient-et vagy az ILogger-t. Ezeket a konstruktora paraméterként kéri, és a MAUI futási idő a DI-konténerből oldja fel őket.
Mielőtt belevágnánk, érdemes megnézned a mi CommunityToolkit.Mvvm útmutatónkat is, mert az MVVM forrásgenerátorok és a DI együtt adják a modern MAUI app gerincét.
Hogyan regisztráljunk szolgáltatásokat a MauiProgram.cs-ben?
Minden DI regisztráció a MauiProgram.CreateMauiApp() metódusban történik. A builder.Services egy IServiceCollection, és ugyanazokat a kiterjesztő metódusokat használjuk, mint az ASP.NET Core-ban: AddSingleton, AddTransient, és (ritkábban) AddScoped. Az alábbi minta egy tipikus startup-fájlt mutat be három rétegnyi regisztrációval: infrastruktúra, üzleti szolgáltatások, és UI komponensek.
using Microsoft.Extensions.Logging;
using CommunityToolkit.Maui;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// 1. Infrastruktura szolgaltatasok (singleton, mert allapot nelkuliek)
builder.Services.AddSingleton<IPreferences>(Preferences.Default);
builder.Services.AddSingleton<IConnectivity>(Connectivity.Current);
builder.Services.AddSingleton<IFileSystem>(FileSystem.Current);
// 2. HTTP es REST kliens
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/v1/");
client.Timeout = TimeSpan.FromSeconds(15);
});
// 3. Uzleti szolgaltatasok
builder.Services.AddSingleton<IUserRepository, UserRepository>();
builder.Services.AddTransient<IOrderService, OrderService>();
// 4. ViewModelek (transient, hogy minden navigacional friss allapot legyen)
builder.Services.AddTransient<LoginViewModel>();
builder.Services.AddTransient<UserProfileViewModel>();
builder.Services.AddTransient<OrderListViewModel>();
// 5. Oldalak (a Shell ezekbol old fel navigaciokor)
builder.Services.AddTransient<LoginPage>();
builder.Services.AddTransient<UserProfilePage>();
builder.Services.AddTransient<OrderListPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}Vegyük észre a sorrendet: először az infrastruktúrát regisztráljuk, mert ezektől függenek az üzleti szolgáltatások, amelyektől viszont a ViewModelek függenek. A konténer maga megoldja a feloldási sorrendet, de logikailag könnyebb így olvasni, és code review során is gyorsabb átlátni. A hivatalos .NET MAUI architektúra-útmutatóban a Microsoft is ezt a struktúrát ajánlja.
Service lifetimes: Singleton, Scoped és Transient
A DI konténer három élettartamot támogat, de a MAUI mobil kontextus alapvetően másképp viselkedik, mint az ASP.NET Core webszerver. Egy webszerveren a Scoped élettartam HTTP kérésenként új példányt jelent, mobilon viszont nincs „request" fogalom, így a Scoped a gyakorlatban majdnem soha nem hasznos egy szabványos MAUI alkalmazásban. Maradunk a Singleton és a Transient mellett.
| Élettartam | Mikor használjuk | Példák | Veszélyek |
|---|---|---|---|
Singleton | Állapot nélküli vagy globális állapotú szolgáltatások | IPreferences, IUserRepository, IConnectivity | Nem hivatkozhat Transient függőségre |
Transient | Minden feloldáskor friss állapot kell | ViewModelek, oldalak, rövid életű parancsok | Memóriafogyasztás, ha nem dispose-oljuk |
Scoped | Ritkán használt MAUI-ban | Egyedi IServiceScope-on belüli munkamenet | Implicit scope nincs, manuálisan kell létrehozni |
A leggyakoribb hiba, amit code review során látunk: valaki egy Singleton szolgáltatásba beinjekciózik egy Transient ViewModelt vagy oldalt. Ez az ún. „captive dependency" probléma. A Singleton megtartja a referenciát, így a Transient élettartama de facto Singleton lesz. Memóriaszivárgáshoz vezet, és a UI-állapot „beragad". Egyik utolsó projektünkben pont ezzel találkoztam: a profil képernyő tartalma nem akart frissülni navigáció után, és csak miután megnéztem a regisztrációkat, lett nyilvánvaló, hogy egy logger-szerű singleton-on keresztül lóg bent a ViewModel.
Hogyan injektáljunk szolgáltatásokat ViewModelekbe?
A konstruktor-injektálás a MAUI standard mintája. A ViewModel a paraméterek listájában kéri, amire szüksége van, és a konténer feloldja őket. A CommunityToolkit.Mvvm ObservableObject bázisosztálya és a forrásgenerátorok tökéletesen együttműködnek a DI-vel.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
public partial class UserProfileViewModel : ObservableObject
{
private readonly IApiClient _apiClient;
private readonly IUserRepository _userRepository;
private readonly ILogger<UserProfileViewModel> _logger;
[ObservableProperty]
private string? _displayName;
[ObservableProperty]
private bool _isBusy;
public UserProfileViewModel(
IApiClient apiClient,
IUserRepository userRepository,
ILogger<UserProfileViewModel> logger)
{
_apiClient = apiClient;
_userRepository = userRepository;
_logger = logger;
}
[RelayCommand]
private async Task LoadProfileAsync()
{
if (IsBusy) return;
try
{
IsBusy = true;
var profile = await _apiClient.GetUserProfileAsync();
DisplayName = profile.Name;
await _userRepository.SaveAsync(profile);
}
catch (Exception ex)
{
_logger.LogError(ex, "Profil betoltese sikertelen");
}
finally
{
IsBusy = false;
}
}
}Vegyük észre: a ViewModel semmit sem tud arról, hogy az IApiClient mögött valójában egy HttpClient-alapú implementáció áll. Cserélhetnénk a teljes hálózati réteget egy in-memory mockra a teszteknél anélkül, hogy a ViewModelhez hozzá kellene nyúlnunk. Ez a DI valódi értéke. Ha REST hívásokkal dolgozol, a .NET MAUI HttpClient útmutatónk mélyebben tárgyalja az IApiClient tervezési mintát.
Page DI Shell navigációval
A .NET MAUI Shell automatikusan használja a DI konténert oldal- és ViewModel-feloldáshoz, ha a Shell Routes-aiban regisztrált útvonalra navigálsz. A trükk az, hogy mind az oldalt, mind a ViewModelt regisztrálni kell a konténerben, és az oldal BindingContext-jét konstruktoron keresztül beállítani.
// UserProfilePage.xaml.cs
public partial class UserProfilePage : ContentPage
{
public UserProfilePage(UserProfileViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}A Shell GoToAsync hívás automatikusan a konténerből kéri ki az oldalt. Ha az oldal vagy a ViewModel nincs regisztrálva, InvalidOperationException: Unable to resolve service hibát kapsz. Ez a leggyakoribb indítási hiba, amit junior MAUI fejlesztők elkövetnek. A Shell navigációs útmutatónk részletesen bemutatja az útvonalak regisztrálását.
Paraméterek átadásánál ne a konstruktorba próbáljuk őket beinjektálni, erre van a QueryProperty attribútum vagy az IQueryAttributable interfész. A konstruktor csak DI-szolgáltatásokat kaphat.
Constructor injection vs. Service Locator
Néha kísértésbe esik az ember, hogy az IPlatformApplication.Current.Services.GetRequiredService<T>() hívással bárhonnan kihúzzon egy szolgáltatást a konténerből. Ez a Service Locator minta, és anti-pattern. Elrejti a függőségeket, megnehezíti a tesztelést, és futási idejű hibákat okoz olyan dolgokra, amik fordításkor kiderülhetnének.
Ennek ellenére van három legitim eset, ahol használjuk:
- Statikus konverterek vagy markup extensions, ahol nincs konstruktor.
- App.xaml.cs vagy más bootstrap kód, ahol a DI még éppen most konfigurálódott.
- Platform-specifikus handler vagy lifecycle event, ahol a MAUI futási idő ad át objektumokat.
// Csak ha tenyleg muszaj, pl. egy MarkupExtension-ben
public class LocalizedStringExtension : IMarkupExtension<string>
{
public string Key { get; set; } = string.Empty;
public string ProvideValue(IServiceProvider serviceProvider)
{
var localizer = IPlatformApplication.Current!
.Services.GetRequiredService<ILocalizationService>();
return localizer.Get(Key);
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
=> ProvideValue(serviceProvider);
}Minden más esetben konstruktor-injektálást használj. A Microsoft DI guideline dokumentációja részletesen indokolja, miért rossz általában a Service Locator.
HttpClient regisztrálása IHttpClientFactory-val
A HttpClient-et soha ne regisztráld AddSingleton<HttpClient>-tal és soha ne példányosítsd ViewModelben. Honestly, ezt a hibát elkövettem én is anno: egy hosszú élettartamú HttpClient elavult DNS bejegyzéseket cache-el (mobilon hálózatváltáskor, Wi-Fi-ről mobil adatra, ez komoly probléma), míg a túl gyakran létrehozott HttpClient socket-kifulladást (SocketException) okoz. A megoldás az IHttpClientFactory.
// MauiProgram.cs-ben
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/v1/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(20);
})
.AddStandardResilienceHandler(); // .NET 8+ retry, timeout, circuit breaker
// Hasznalat a tipusolt kliensben
public class ApiClient : IApiClient
{
private readonly HttpClient _http;
public ApiClient(HttpClient http) => _http = http;
public async Task<UserProfile> GetUserProfileAsync()
{
var response = await _http.GetFromJsonAsync<UserProfile>("me");
return response ?? throw new InvalidOperationException("Ures valasz");
}
}Az AddStandardResilienceHandler() a Polly-alapú újrapróbálási, timeout és circuit-breaker viselkedést adja hozzá egy sorral. A mi tapasztalatunk szerint: ha kihagyod, az első instabil hálózatú felhasználói visszajelzéseknél vissza fogod tenni. Tényleg.
Tesztelhetőség: mockolás és unit teszt minták
A DI legnagyobb gyakorlati haszna nem az architektúrai szépség, hanem a tesztelhetőség. Egy konstruktor-injektált ViewModelt mocked függőségekkel pillanatok alatt tesztelhetünk, és a DI-konténer maga teljesen kimarad az unit tesztből. Az alábbi példa xUnit + NSubstitute kombinációt használ, ami a csapatomban a standard.
using NSubstitute;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
public class UserProfileViewModelTests
{
[Fact]
public async Task LoadProfile_SikeresValasz_BeallitjaADisplayNeve()
{
// Arrange
var apiClient = Substitute.For<IApiClient>();
var userRepo = Substitute.For<IUserRepository>();
apiClient.GetUserProfileAsync()
.Returns(new UserProfile { Name = "Kovacs Anna" });
var sut = new UserProfileViewModel(
apiClient,
userRepo,
NullLogger<UserProfileViewModel>.Instance);
// Act
await sut.LoadProfileCommand.ExecuteAsync(null);
// Assert
Assert.Equal("Kovacs Anna", sut.DisplayName);
Assert.False(sut.IsBusy);
await userRepo.Received(1).SaveAsync(Arg.Any<UserProfile>());
}
}Vegyük észre: nincs MauiApp, nincs ServiceProvider, nincs platform-specifikus inicializálás. Egy egyszerű .NET osztálykönyvtárban fut, milliszekundumok alatt. Ez akkor lehetséges, ha minden függőség interfész mögött van. Ez a fegyelem hozza a DI valódi nyereségét.
Gyakori hibák és teljesítménytippek
A produkciós tapasztalatunk alapján öt hiba ismétlődik a leggyakrabban DI-vel dolgozó MAUI projektekben, érdemes előre tudni róluk.
1. Elfelejtett oldal- vagy ViewModel-regisztráció
Hibaüzenet: Unable to resolve service for type 'MyApp.OrderListViewModel'. Mindig regisztráld mind az oldalt, mind a ViewModelt a MauiProgram.cs-ben.
2. Singleton ViewModel
Tünet: navigáció után a régi adatok láthatók. Megoldás: ViewModel-ek mindig AddTransient.
3. Captive dependency
Tünet: emelkedő memóriahasználat, ViewModelek nem GC-zelődnek. Soha ne injektálj Transient szolgáltatást Singleton-ba közvetlenül. Helyette használj IServiceProvider-t és scope-ot, ha tényleg muszáj.
4. Indítási idő
Minden AddSingleton az alkalmazás teljes futási ideje alatt él, de a regisztráció maga lusta: a példány csak az első feloldáskor jön létre. Ha nehéz inicializáció van valami szolgáltatásban (pl. SQLite kapcsolat), érdemes AsyncLazy<T>-be csomagolni. Részletesebben a SQLite helyi adatbázis útmutatónkban tárgyaljuk az inicializálási stratégiákat.
5. Generikus szolgáltatások
A nyitott generikusok regisztrálása kicsit más szintaxis: builder.Services.AddSingleton(typeof(IRepository<>), typeof(Repository<>));. A szögletes zárójelek közt semmi nincs, ez nem elgépelés.
Gyakran Ismételt Kérdések
Mi a különbség a Singleton és a Transient között .NET MAUI-ban?
A Singleton egy példányt hoz létre az alkalmazás teljes életciklusára, és minden injektálási helyen ugyanazt a példányt adja vissza. A Transient minden feloldáskor új példányt készít. MAUI-ban a háttérszolgáltatásokat (repo, API kliens) Singleton-ként, a ViewModeleket és oldalakat Transient-ként regisztráljuk.
Miért kapok „Unable to resolve service" hibát .NET MAUI-ban?
Mert az oldal vagy ViewModel nincs regisztrálva a MauiProgram.cs-ben. Add hozzá builder.Services.AddTransient<YourPage>() és builder.Services.AddTransient<YourViewModel>() sorokkal. A Shell navigáció a DI-konténerből oldja fel az oldalakat, így a regisztráció kötelező.
Használhatok Scoped élettartamot .NET MAUI-ban?
Technikailag igen, de a gyakorlatban majdnem soha nem hasznos. A MAUI-ban nincs ASP.NET Core-szerű implicit „request scope", így a Scoped szolgáltatás úgy viselkedik, mint a Transient, kivéve, ha manuálisan hozol létre IServiceScope-ot. Maradj a Singleton és Transient mellett.
Hogyan injektáljam a HttpClient-et MAUI-ban?
Ne közvetlenül. Használj AddHttpClient<IApiClient, ApiClient>()-t típusolt klienssel. Ez integrálja az IHttpClientFactory-t, kezeli a DNS cache-t, a socket élettartamot, és lehetővé teszi a Polly-alapú resilience handler hozzáadását egy sorral.
Kell-e DI konténer az unit tesztekhez?
Nem. Egy unit teszt csak a tesztelt osztály konstruktorát hívja, és kézzel ad át mock függőségeket. A DI konténer csak az alkalmazás futási idejének része, tesztben nincs szükség MauiApp-ra vagy ServiceProvider-re. Integrációs tesztben ez más, de unit tesztben felesleges komplexitás.




