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.
A .NET MAUI a Microsoft.Extensions.DependencyInjection konténert használja, ugyanazt, amit az ASP.NET Core. A MauiAppBuilder.Services egy szabványos IServiceCollection.
Három élettartam létezik: Singleton (egy példány az alkalmazás teljes életciklusára), Scoped (ritkán használt MAUI-ban), és Transient (új példány minden feloldáskor).
Az oldalakat és ViewModeleket regisztrálni kell a konténerben, hogy a Shell és a konstruktor-injektálás működjön, különben InvalidOperationException-t kapunk indításkor.
A Singleton-ok és UI elemek (Page, ViewModel) keverése a leggyakoribb memóriaszivárgási forrás. A hosszú élettartamú szolgáltatások nem hivatkozhatnak rövid élettartamúakra.
A HttpClient-et mindig IHttpClientFactory-val regisztráljuk, ne AddSingleton<HttpClient>-tal, különben DNS-cache problémák jönnek elő.
Teszteléshez használjunk interfészeket és Moq-ot vagy NSubstitute-ot. A DI-konténer egy unit teszt számára nem szükséges.
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.MvvmObservableObject bázisosztálya és a forrásgenerátorok tökéletesen együttműködnek a DI-vel.
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.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.
A CommunityToolkit.Mvvm forrásgenerátorai 60-70%-kal rövidebbé teszik a .NET MAUI ViewModel-eket. Gyakorlati útmutató ObservableProperty, RelayCommand, Messenger és validáció témákban éles projektből származó tippekkel.
Ismerd meg, hogyan integrálhatsz SQLite adatbázist .NET MAUI alkalmazásodba: az SQLite-net beállítástól a CRUD műveleteken át az MVVM összekötésig, teljesítményoptimalizálásig és offline szinkronig, működő kódpéldákkal.