Dependency Injection în .NET MAUI 2026: Ghid Complet cu Microsoft.Extensions.DependencyInjection

Învață cum să folosești Dependency Injection în .NET MAUI 9 și 10 cu Microsoft.Extensions.DependencyInjection: cicluri de viață, keyed services, IHttpClientFactory și cele mai frecvente cinci greșeli, cu cod C# funcțional.

Dependency Injection .NET MAUI 2026

Actualizat: 19 august 2026

Dependency Injection (DI) în .NET MAUI este un pattern nativ, integrat prin Microsoft.Extensions.DependencyInjection, care îți permite să înregistrezi servicii pe MauiAppBuilder.Services și să le injectezi în pagini, ViewModel-uri și handler-e prin constructor. Începând cu .NET MAUI 9 și continuând în .NET 10 (LTS, noiembrie 2025), containerul DI suportă keyed services, IHttpClientFactory, scoped services per fereastră și rezolvare tipizată, așa că nu mai ai nevoie de DependencyService-ul moștenit din Xamarin.Forms.

  • .NET MAUI folosește containerul Microsoft.Extensions.DependencyInjection înregistrat pe builder.Services în MauiProgram.cs.
  • Există trei cicluri de viață (Singleton, Scoped, Transient), iar alegerea greșită este cauza #1 a memory leak-urilor și a bug-urilor de tip „stale state”.
  • Paginile și ViewModel-urile trebuie înregistrate explicit ca să beneficiezi de injectare prin constructor; altfel va trebui să le rezolvi manual din IServiceProvider.
  • Keyed services (disponibile din .NET 8+) permit înregistrarea mai multor implementări pentru aceeași interfață, util pentru strategii, tenant-i sau feature flags.
  • Fiecare Window primește propriul IServiceScope, așa că serviciile Scoped funcționează corect în aplicații multi-fereastră (desktop, iPad).
  • DependencyService din Xamarin.Forms e obsolet. Nu îl folosi în cod nou; migrează la containerul standard.

Ce este Dependency Injection în .NET MAUI?

Dependency Injection este un pattern prin care un obiect nu își creează singur dependințele, ci le primește din exterior, de obicei prin constructor. În .NET MAUI, această funcționalitate e oferită de Microsoft.Extensions.DependencyInjection, același container folosit de ASP.NET Core, Blazor și serviciile Worker. Diferența față de Xamarin.Forms e că nu mai există un container proprietar (DependencyService) cu service location, ci un container standard bazat pe constructor injection.

Așa că, atunci când aplicația pornește, MauiProgram.CreateMauiApp() construiește un MauiAppBuilder care expune colecția IServiceCollection. Aici înregistrezi toate serviciile: clienți HTTP, repository-uri SQLite, IStringLocalizer, ViewModel-uri. La runtime, când o pagină este cerută, containerul construiește arborele de dependințe recursiv, rezolvând toți constructorii până când toți parametrii sunt satisfăcuți.

Sincer, marele avantaj practic este testabilitatea. În teste unitare poți înlocui IApiClient cu un mock fără să atingi codul aplicației. În plus, DI elimină singleton-urile globale scrise manual, cuplarea rigidă între layer-e și codul de tip new HttpClient() presărat prin ViewModel-uri.

Cum înregistrezi servicii în MauiProgram.cs

Punctul central de configurare este metoda CreateMauiApp() din MauiProgram.cs. Aici apelezi metode de extensie de tipul AddSingleton, AddTransient sau AddScoped pe builder.Services. Ordinea contează doar pentru keyed services și pentru IEnumerable<T>; în rest, containerul rezolvă întotdeauna ultima înregistrare pentru un tip dat.

// MauiProgram.cs (.NET MAUI 9 / 10)
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;

namespace ContosoApp;

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. Servicii de infrastructura (o singura instanta pe aplicatie)
        builder.Services.AddSingleton<IConnectivity>(Connectivity.Current);
        builder.Services.AddSingleton<IPreferences>(Preferences.Default);
        builder.Services.AddSingleton<ISecureStorage>(SecureStorage.Default);

        // 2. Servicii custom
        builder.Services.AddSingleton<IProductRepository, SqliteProductRepository>();
        builder.Services.AddSingleton<IAuthService, AuthService>();

        // 3. HttpClient tipizat (factory-based, evita socket exhaustion)
        builder.Services.AddHttpClient<IContosoApi, ContosoApiClient>(client =>
        {
            client.BaseAddress = new Uri("https://api.contoso.com/");
            client.Timeout = TimeSpan.FromSeconds(30);
        });

        // 4. ViewModel-uri: Transient pentru izolare per navigare
        builder.Services.AddTransient<ProductsViewModel>();
        builder.Services.AddTransient<ProductDetailViewModel>();

        // 5. Pagini: Transient, corelate cu ViewModel-urile
        builder.Services.AddTransient<ProductsPage>();
        builder.Services.AddTransient<ProductDetailPage>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Singleton vs Scoped vs Transient: care este diferența?

Cel mai important lucru pe care trebuie să îl înțelegi înainte să scrii AddSomething este ciclul de viață. Alegerea greșită duce fie la memory leaks (un Singleton care ține referințe la ViewModel-uri distruse), fie la state bugs (Transient acolo unde te așteptai la o instanță comună). Într-un audit recent am dat de un Singleton care ținea în viață trei pagini închise. Aplicația crăpa după 40 de minute pe device-uri cu 3 GB RAM. Tabelul de mai jos rezumă comportamentul așa cum îl implementează Microsoft.Extensions.DependencyInjection în .NET 9 și .NET 10.

CaracteristicăSingletonScopedTransient
Instanțe per aplicație11 per scopeN (una la fiecare cerere)
Când este creatPrima rezolvare, apoi cachedPrima rezolvare în scopeLa fiecare rezolvare
Când este DisposedLa închiderea aplicațieiLa eliminarea scope-uluiLa eliminarea scope-ului părinte
Sigur pentru state globalDa (cu grijă la thread-safety)Doar per fereastră/scopeNu, instanță nouă
Potrivit pentruConfig, cache, HTTP clients (factory)DbContext, unit-of-work per fereastrăViewModel-uri, comenzi, servicii ușoare
Riscul principalMemory leak dacă ține referințe UIConfuzie cu Singleton într-o singură fereastrăAlocări excesive

Regula mea empirică (funcționează în ~90% din cazuri): ViewModel-urile sunt Transient, repository-urile și serviciile de date sunt Singleton, iar DbContext-ul sau unitățile de lucru sunt Scoped. În .NET MAUI, fiecare fereastră (Window) primește automat un scope propriu, deci serviciile Scoped funcționează corect în aplicațiile multi-window (iPad, Mac Catalyst, WinUI).

Cum injectezi servicii în ViewModel-uri și pagini

Odată ce serviciile și paginile sunt înregistrate, injectarea este trivială: declari parametrii în constructor și containerul îi umple. Ideea este că pagina primește ViewModel-ul, iar ViewModel-ul primește serviciile de care are nevoie. Nu instanția niciodată manual un ViewModel cu new ProductsViewModel(), pentru că vei pierde tot arborele DI.

// Services/IProductRepository.cs
public interface IProductRepository
{
    Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken ct = default);
    Task<Product?> GetByIdAsync(int id, CancellationToken ct = default);
}

// ViewModels/ProductsViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class ProductsViewModel : ObservableObject
{
    private readonly IProductRepository _repository;
    private readonly IConnectivity _connectivity;
    private readonly ILogger<ProductsViewModel> _logger;

    [ObservableProperty]
    private ObservableCollection<Product> products = new();

    [ObservableProperty]
    private bool isBusy;

    public ProductsViewModel(
        IProductRepository repository,
        IConnectivity connectivity,
        ILogger<ProductsViewModel> logger)
    {
        _repository = repository;
        _connectivity = connectivity;
        _logger = logger;
    }

    [RelayCommand]
    private async Task LoadAsync(CancellationToken ct)
    {
        if (_connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            _logger.LogWarning("Offline, folosesc cache local");
        }

        try
        {
            IsBusy = true;
            var items = await _repository.GetAllAsync(ct);
            Products = new ObservableCollection<Product>(items);
        }
        finally { IsBusy = false; }
    }
}

// Views/ProductsPage.xaml.cs
public partial class ProductsPage : ContentPage
{
    public ProductsPage(ProductsViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

Pentru navigarea între pagini, folosește Shell.Current.GoToAsync("ProductDetail"). Shell rezolvă pagina din DI automat, atâta timp cât ai înregistrat ruta cu Routing.RegisterRoute("ProductDetail", typeof(ProductDetailPage)). Pentru o discuție mai amănunțită despre rute și deep linking, vezi ghidul de navigare Shell în .NET MAUI.

Keyed services și pattern-ul Strategy

Începând cu .NET 8, containerul standard suportă keyed services: mai multe implementări ale aceleiași interfețe, diferențiate printr-o cheie (string, enum sau orice obiect). Este soluția potrivită pentru scenarii precum autentificare per tenant, feature flags sau strategii de plată. Înainte de .NET 8 aveai nevoie de biblioteci terțe (Autofac, Simple Injector) pentru asta.

// Inregistrare keyed
builder.Services.AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe");
builder.Services.AddKeyedSingleton<IPaymentGateway, PayPalGateway>("paypal");
builder.Services.AddKeyedSingleton<IPaymentGateway, RevolutGateway>("revolut");

// Injectare cu [FromKeyedServices]
public class CheckoutViewModel
{
    private readonly IPaymentGateway _gateway;

    public CheckoutViewModel(
        [FromKeyedServices("stripe")] IPaymentGateway gateway)
    {
        _gateway = gateway;
    }
}

// Rezolvare dinamica din IServiceProvider
public class PaymentSelector
{
    private readonly IServiceProvider _sp;
    public PaymentSelector(IServiceProvider sp) => _sp = sp;

    public IPaymentGateway GetGatewayFor(string countryCode) => countryCode switch
    {
        "US" => _sp.GetRequiredKeyedService<IPaymentGateway>("stripe"),
        "RO" => _sp.GetRequiredKeyedService<IPaymentGateway>("revolut"),
        _    => _sp.GetRequiredKeyedService<IPaymentGateway>("paypal"),
    };
}

Keyed services elimină lanțurile lungi de if/else și pattern-ul ServiceLocator pe care mulți dezvoltatori îl construiau manual. Pentru scenarii MVVM, poți combina keyed services cu source generator-ul CommunityToolkit.Mvvm, așa cum arată ghidul MVVM în .NET MAUI cu CommunityToolkit.

Cum rezolvi manual servicii din IServiceProvider

Uneori nu poți folosi injectare prin constructor. Cazuri clasice: un handler de MessagingCenter, un converter XAML sau un cod static de migrare la startup. Pentru aceste situații, .NET MAUI expune IPlatformApplication.Current.Services, un helper accesibil global. Folosește-l cu moderație, pentru că abuzul devine anti-pattern-ul Service Locator.

// Rezolvare din cod static (ex: un migrator de baza de date la startup)
public static class Bootstrapper
{
    public static async Task RunMigrationsAsync()
    {
        var sp = IPlatformApplication.Current!.Services;
        var repo = sp.GetRequiredService<IProductRepository>();
        await repo.EnsureCreatedAsync();
    }
}

// Rezolvare cu scope explicit (pentru servicii Scoped in cod non-UI)
public static async Task ProcessJobAsync()
{
    var sp = IPlatformApplication.Current!.Services;
    await using var scope = sp.CreateAsyncScope();
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.SaveChangesAsync();
}

Integrarea cu IHttpClientFactory și Refit

Un anti-pattern frecvent în aplicațiile mobile este new HttpClient() la fiecare cerere. Pe Android duce la socket exhaustion după câteva sute de apeluri (am întâlnit exact acest bug într-un proiect de fintech, era distractiv de debugat la 2 dimineața), iar pe iOS ignoră politica de proxy a sistemului. Soluția corectă este IHttpClientFactory, care reutilizează HttpMessageHandler-ele și se integrează perfect în containerul DI.

// MauiProgram.cs
builder.Services.AddHttpClient<IContosoApi, ContosoApiClient>(client =>
{
    client.BaseAddress = new Uri("https://api.contoso.com/v2/");
    client.DefaultRequestHeaders.Add("User-Agent", "ContosoApp/2.4");
})
.AddStandardResilienceHandler(); // Retry + circuit breaker (Polly, .NET 8+)

// Cu Refit (API declarativ)
builder.Services
    .AddRefitClient<IContosoApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.contoso.com/v2/"));

Pachetul Microsoft.Extensions.Http.Resilience aduce retry-uri, circuit breaker și timeout-uri tipizate. Perfect pentru rețelele mobile instabile. Este suportat oficial pe .NET 8 și .NET 10, iar înlocuiește configurările manuale Polly.

Greșeli frecvente și cum le eviți

În auditurile pe care le fac pe aplicații MAUI, aceleași cinci greșeli DI reapar constant. Le enumăr aici cu simptomele și corecția, în ordinea frecvenței cu care le văd la echipele care migrează de la Xamarin sau învață .NET MAUI pentru prima dată.

  1. Capturarea unui Scoped într-un Singleton. Simptom: InvalidOperationException: Cannot consume scoped service from singleton. Fix: fie ridici serviciul dependent la Singleton, fie folosești IServiceScopeFactory pentru a crea un scope pe cerere.
  2. Instanțiere manuală cu new. Simptom: dependințele sunt null, comenzile nu execută. Fix: înregistrează pagina/ViewModel-ul în DI și lasă Shell sau App.Current.Windows[0].Page.Handler să le rezolve.
  3. Uitarea înregistrării unei pagini. Simptom: MissingMethodException: No parameterless constructor. Fix: adaugă AddTransient<MyPage>() în MauiProgram.cs.
  4. Singleton care ține referințe la UI. Simptom: memory leak, ferestrele nu sunt colectate. Fix: expune evenimente slab-referenced (WeakEventManager) sau folosește MessagingCenter/CommunityToolkit Messenger cu weak references.
  5. Servicii circulare. A depinde de B, care depinde de A. Fix: extrage interfața comună într-un al treilea serviciu, sau folosește Lazy<T> pentru a rupe ciclul.

Migrare de la DependencyService (Xamarin.Forms)

Xamarin.Forms.DependencyService este marcat ca obsolete în .NET MAUI și va fi eliminat complet în versiuni viitoare. Migrarea este directă: înlocuiești atributele [assembly: Dependency] cu înregistrări explicite pe builder.Services. Nu mai există implementări per-platformă rezolvate prin reflecție; totul este vizibil și tipizat la compile time.

// Vechi (Xamarin.Forms)
[assembly: Dependency(typeof(DroidBatteryService))]
public class DroidBatteryService : IBatteryService { /* ... */ }

// Uzaj vechi
var battery = DependencyService.Get<IBatteryService>();

// Nou (.NET MAUI): MauiProgram.cs, per platforma
#if ANDROID
builder.Services.AddSingleton<IBatteryService, Platforms.Android.DroidBatteryService>();
#elif IOS
builder.Services.AddSingleton<IBatteryService, Platforms.iOS.IosBatteryService>();
#endif

// Uzaj nou: injectare prin constructor
public class DeviceViewModel(IBatteryService battery) { /* ... */ }

Pentru un ghid pas cu pas al întregii migrări (inclusiv Handler-e, Effect-e și proiectele partajate), consultă migrarea de la Xamarin.Forms la .NET MAUI. Iar dacă vrei să te uiți la sursele oficiale, containerul folosit este cel din repo-ul dotnet/runtime, Microsoft.Extensions.DependencyInjection.

Întrebări frecvente

Are .NET MAUI Dependency Injection nativ?

Da. .NET MAUI folosește containerul Microsoft.Extensions.DependencyInjection integrat prin MauiAppBuilder.Services în MauiProgram.cs. Nu ai nevoie de biblioteci externe pentru scenarii comune; Autofac sau alte containere sunt necesare doar pentru feature-uri avansate precum property injection sau interception.

Care este diferența dintre AddSingleton, AddScoped și AddTransient?

Singleton creează o singură instanță pentru toată durata de viață a aplicației. Scoped creează o instanță per scope, iar în .NET MAUI un scope corespunde unei ferestre (Window). Transient creează o instanță nouă la fiecare rezolvare. Regula: repository-uri = Singleton, DbContext = Scoped, ViewModel-uri = Transient.

Cum injectez un serviciu într-un ViewModel MAUI?

Înregistrează ViewModel-ul cu builder.Services.AddTransient<MyViewModel>(), declară dependințele ca parametri de constructor, apoi lasă containerul să le umple când pagina care setează BindingContext = viewModel este rezolvată de Shell. Nu instanția manual ViewModel-ul cu new.

Pot folosi keyed services în .NET MAUI?

Da, începând cu .NET 8 (inclusiv .NET 9 și .NET 10). Folosește AddKeyedSingleton<IService, Impl>("cheie") pentru înregistrare și atributul [FromKeyedServices("cheie")] pe parametrul de constructor pentru injectare. Pentru rezolvare dinamică, folosește IServiceProvider.GetRequiredKeyedService<T>("cheie").

De ce primesc InvalidOperationException „Cannot consume scoped service from singleton”?

Ai injectat un serviciu Scoped într-un serviciu Singleton. Singleton-ul ar captura scope-ul primei rezolvări și l-ar ține în viață. Fie ridici serviciul consumator la Scoped, fie injectezi IServiceScopeFactory și creezi un scope explicit pe fiecare operațiune care are nevoie de acel serviciu.

Cum înlocuiesc DependencyService din Xamarin.Forms în .NET MAUI?

Înlocuiește atributele [assembly: Dependency(typeof(Impl))] cu înregistrări explicite pe builder.Services, folosind directive #if ANDROID / #if IOS pentru implementările per-platformă. Apoi înlocuiește DependencyService.Get<T>() cu injectare prin constructor. Este mai mult cod, dar tipizat, verificat la compilare și testabil.

Editorial Team
Despre Autor Editorial Team

Our team of expert writers and editors.