Compartilhar via


Injeção de dependência

A .NET Multi-Platform App UI (.NET MAUI) fornece suporte interno para usar a injeção de dependência. A injeção de dependência é uma versão especializada do padrão de Inversão de Controle (IoC), em que a preocupação que está sendo invertida é o processo de obtenção da dependência necessária. Com a injeção de dependência, outra classe é responsável por injetar dependências em um objeto em runtime.

Normalmente, um construtor de classe é invocado ao instanciar um objeto e todos os valores necessários pelo objeto são passados como argumentos para o construtor. Este é um exemplo de injeção de dependência conhecida como injeção de construtor. As dependências de que o objeto precisa são injetadas no construtor.

Observação

Há também outros tipos de injeção de dependência, como injeção de setter de propriedade e injeção de chamada de método, mas eles são menos usados.

Ao especificar dependências como tipos de interface, a injeção de dependência permite desacoplar os tipos concretos do código que depende desses tipos. Geralmente, ele usa um contêiner que contém uma lista de registros e mapeamentos entre interfaces e tipos abstratos e os tipos concretos que implementam ou estendem esses tipos.

Contêineres de injeção de dependência

Se uma classe não instanciar diretamente os objetos necessários, outra classe deverá assumir essa responsabilidade. Considere o exemplo a seguir, que mostra uma classe de modelo de exibição que requer argumentos de construtor:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
    {
        _loggingService = loggingService;
        _settingsService = settingsService;
    }
}

Neste exemplo, o construtor MainPageViewModel requer duas instâncias de objeto de interface como argumentos injetados por outra classe. A única dependência na classe MainPageViewModel é nos tipos de interface. Portanto, a classe MainPageViewModel não tem nenhum conhecimento da classe responsável por instanciar os objetos de interface.

Da mesma forma, considere o exemplo a seguir que mostra uma classe de página que requer um argumento de construtor:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

Neste exemplo, o construtor MainPage requer um tipo concreto como um argumento que é injetado por outra classe. A única dependência na classe MainPage está no tipo MainPageViewModel. Portanto, a classe MainPage não tem nenhum conhecimento da classe responsável por instanciar o tipo concreto.

Em ambos os casos, a classe responsável por instanciar as dependências e inseri-las na classe dependente é conhecida como o contêiner de injeção de dependência.

Os contêineres de injeção de dependência reduzem o acoplamento entre objetos fornecendo uma instalação para instanciar instâncias de classe e gerenciar seu tempo de vida com base na configuração do contêiner. Durante a criação do objeto, o contêiner injeta todas as dependências necessárias pelo objeto. Se essas dependências não tiverem sido criadas, o contêiner criará e resolverá suas dependências primeiro.

Há várias vantagens em usar um contêiner de injeção de dependência:

  • Um contêiner remove a necessidade de uma classe localizar suas dependências e gerenciar seus tempos de vida.
  • Um contêiner permite o mapeamento de dependências implementadas sem afetar a classe.
  • Um contêiner facilita a capacidade de teste permitindo que as dependências sejam simuladas.
  • Um contêiner aumenta a capacidade de manutenção, permitindo que novas classes sejam facilmente adicionadas ao aplicativo.

No contexto de um aplicativo .NET MAUI que usa o padrão MVVM (Model-View-ViewModel), um contêiner de injeção de dependência normalmente será usado para registrar e resolver exibições, registrar e resolver modelos de exibição e registrar serviços e injetá-los em modelos de exibição. Para obter mais informações sobre o padrão MVVM, consulte Model-View-ViewModel (MVVM).

Há muitos contêineres de injeção de dependência disponíveis para .NET. O .NET MAUI tem suporte interno para usar Microsoft.Extensions.DependencyInjection para gerenciar a instanciação de exibições, modelos de exibição e classes de serviço em um aplicativo. Microsoft.Extensions.DependencyInjection facilita a criação de aplicativos acoplados de forma flexível e fornece todos os recursos comumente encontrados em contêineres de injeção de dependência, incluindo métodos para registrar mapeamentos de tipo e instâncias de objeto, resolver objetos, gerenciar tempos de vida de objetos e injetar objetos dependentes em construtores de objetos que ele resolve. Para mais informações sobre Microsoft.Extensions.DependencyInjection confira Injeção de dependência no .NET.

Em runtime, o contêiner deve saber qual implementação das dependências estão sendo solicitadas para instanciá-las para os objetos solicitados. No exemplo acima, as interfaces ILoggingService e ISettingsService precisam ser resolvidas antes que o objeto MainPageViewModel possa ser instanciado. Isso envolve o contêiner executando as seguintes ações:

  • Decidindo como instanciar um objeto que implementa a interface. Isso é conhecido como registro. Para saber mais, confira Registro.
  • Instanciando o objeto que implementa a interface necessária e o objeto MainPageViewModel. Isso é conhecido como resolução. Para obter mais informações, consulte Resolução.

Eventualmente, um aplicativo terminará de usar o objeto MainPageViewModel e ficará disponível para coleta de lixo. Neste ponto, o coletor de lixo deverá descartar quaisquer implementações de interface de curta duração se outras classes não compartilharem as mesmas instâncias.

Registro

Antes que as dependências possam ser injetadas em um objeto, os tipos para as dependências devem primeiro ser registrados com o contêiner. O registro de um tipo normalmente envolve passar um tipo concreto ao contêiner ou uma interface e um tipo concreto que implementa a interface.

Há duas abordagens principais para registrar tipos e objetos com o contêiner:

  • Registre um tipo ou mapeamento com o contêiner. Isso é conhecido como registro transitório. Quando necessário, o contêiner criará uma instância do tipo especificado.
  • Registre um objeto existente no contêiner como um singleton. Quando necessário, o contêiner retornará uma referência ao objeto existente.

Cuidado

Contêineres de injeção de dependência nem sempre são adequados para um aplicativo .NET MAUI. A injeção de dependência introduz complexidade e requisitos adicionais que podem não ser apropriados ou úteis para aplicativos menores. Se uma classe não tiver dependências ou não for uma dependência para outros tipos, talvez não faça sentido colocá-la no contêiner. Além disso, se uma classe tiver um único conjunto de dependências que são integrais ao tipo e nunca serão alteradas, talvez não faça sentido colocá-las no contêiner.

O registro de tipos que exigem injeção de dependência deve ser executado em um único método em seu aplicativo. Esse método deve ser invocado no início do ciclo de vida do aplicativo para garantir que ele esteja ciente das dependências entre suas classes. Normalmente, os aplicativos devem executar isso no método CreateMauiApp na classe MauiProgram. A classe MauiProgram chama o método CreateMauiApp para criar um objeto MauiAppBuilder. O objeto MauiAppBuilder tem uma propriedade Services do tipo IServiceCollection, que fornece um local para registrar seus tipos, como exibições, modelos de exibição e serviços para injeção de dependência:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        builder.Services.AddTransient<ILoggingService, LoggingService>();
        builder.Services.AddTransient<ISettingsService, SettingsService>();
        builder.Services.AddSingleton<MainPageViewModel>();
        builder.Services.AddSingleton<MainPage>();

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

        return builder.Build();
    }
}

Os tipos registrados com a propriedade Services são fornecidos para o contêiner de injeção de dependência quando MauiAppBuilder.Build() é chamado.

Ao registrar dependências, você precisa registrar todas as dependências, incluindo todos os tipos que exigem as dependências. Portanto, se você tiver um modelo de exibição que usa uma dependência como um parâmetro de construtor, será necessário registrar o modelo de exibição junto com todas as suas dependências. Da mesma forma, se você tiver uma exibição que usa uma dependência de modelo de exibição como um parâmetro de construtor, será necessário registrar a exibição e o modelo de exibição junto com todas as suas dependências.

Dica

Um contêiner de injeção de dependência é ideal para criar instâncias de modelo de exibição. Se um modelo de exibição tiver dependências, ele gerenciará a criação e a injeção de quaisquer serviços necessários. Basta garantir que você registre seus modelos de exibição e quaisquer dependências que eles possam ter no método CreateMauiApp na classe MauiProgram.

Tempo de vida de dependência

Dependendo das necessidades do seu aplicativo, talvez seja necessário registrar dependências com diferentes tempos de vida. A tabela a seguir lista os principais métodos que você pode usar para registrar dependências e seus tempos de vida de registro:

Método Descrição
AddSingleton<T> Cria uma única instância do objeto que permanecerá durante o tempo de vida do aplicativo.
AddTransient<T> Cria uma nova instância do objeto quando solicitado durante a resolução. Objetos transitórios não têm um tempo de vida predefinido, mas normalmente seguirão o tempo de vida do host.
AddScoped<T> Cria uma instância do objeto que compartilha o tempo de vida de seu host. Quando o host sai do escopo, a dependência também sai. Portanto, resolver a mesma dependência várias vezes dentro do mesmo escopo gera a mesma instância, enquanto resolver a mesma dependência em escopos diferentes produzirá instâncias diferentes.

Observação

Se um objeto não herdar de uma interface, como um modo de exibição ou um modelo de exibição, apenas seu tipo concreto precisará ser fornecido para o método AddSingleton<T>, AddTransient<T> ou AddScoped<T>.

A classe MainPageViewModel é usada perto da raiz do aplicativo e deve estar sempre disponível, portanto, registrá-la com AddSingleton<T> é benéfico. Outros modelos de exibição podem ser navegados ou usados posteriormente em um aplicativo. Se você tiver um tipo que pode nem sempre ser usado, ou se for memória ou com uso intensivo computacional ou exigir dados just-in-time, ele poderá ser um candidato melhor para o registro AddTransient<T>.

Outra maneira comum de registrar dependências é usando os métodos AddSingleton<TService, TImplementation>, AddTransient<TService, TImplementation> ou AddScoped<TService, TImplementation>. Esses métodos assumem dois tipos: a definição da interface e a implementação concreta. Esse tipo de registro é melhor para casos em que você está implementando serviços com base em interfaces.

Depois que todos os tipos tiverem sido registrados, MauiAppBuilder.Build() deve ser chamado para criar o objeto MauiApp e preencher o contêiner de injeção de dependência com todos os tipos registrados.

Importante

Depois que MauiAppBuilder.Build() é chamado, os tipos registrados com o contêiner de injeção de dependência serão imutáveis e não poderão mais ser atualizados ou modificados.

Registrar dependências com um método de extensão

O método MauiApp.CreateBuilder cria um objeto MauiAppBuilder que pode ser usado para registrar dependências. Se o aplicativo precisar registrar muitas dependências, você poderá criar métodos de extensão para ajudar a fornecer um fluxo de trabalho de registro organizado e sustentável:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            .RegisterServices()
            .RegisterViewModels()
            .RegisterViews()
            .Build();

    public static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddTransient<ILoggingService, LoggingService>();
        mauiAppBuilder.Services.AddTransient<ISettingsService, SettingsService>();

        // More services registered here.

        return mauiAppBuilder;        
    }

    public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddSingleton<MainPageViewModel>();

        // More view-models registered here.

        return mauiAppBuilder;        
    }

    public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddSingleton<MainPage>();

        // More views registered here.

        return mauiAppBuilder;        
    }
}

Neste exemplo, os três métodos de extensão de registro usam a instância MauiAppBuilder para acessar a propriedade Services para registrar dependências.

Resolução

Depois que um tipo é registrado, ele pode ser resolvido ou injetado como uma dependência. Quando um tipo está sendo resolvido e o contêiner precisa criar uma nova instância, ele injeta todas as dependências na instância.

Geralmente, quando um tipo é resolvido, um dos três cenários ocorre:

  1. Se o tipo não tiver sido registrado, o contêiner gerará uma exceção.
  2. Se o tipo tiver sido registrado como singleton, o contêiner retornará a instância singleton. Se essa for a primeira vez que o tipo for chamado, o contêiner o criará se necessário e manterá uma referência a ele.
  3. Se o tipo tiver sido registrado como transitório, o contêiner retornará uma nova instância e não manterá uma referência a ela.

O .NET MAUI dá suporte à resolução de dependência automática e explícita. A resolução automática de dependência usa a injeção de construtor sem solicitar explicitamente a dependência do contêiner. A resolução explícita de dependência ocorre sob demanda solicitando explicitamente uma dependência do contêiner.

Resolução automática de dependência

A resolução automática de dependência ocorre em aplicativos que usam o Shell do .NET MAUI, desde que você tenha registrado o tipo da dependência e o tipo que usa a dependência com o contêiner de injeção de dependência.

Durante a navegação baseada em Shell, o .NET MAUI procurará registros de página e, se algum for encontrado, criará essa página e injetará quaisquer dependências em seu construtor:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

Neste exemplo, o construtor MainPage recebe uma instância MainPageViewModel que é injetada. Por sua vez, a instância MainPageViewModel tem as instâncias ILoggingService eISettingsService injetadas:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
    {
        _loggingService = loggingService;
        _settingsService = settingsService;
    }
}

Além disso, em um aplicativo baseado em Shell, o .NET MAUI injetará dependências em páginas detalhadas registradas com o método Routing.RegisterRoute.

Resolução de dependência explícita

Um aplicativo baseado em Shell não pode usar injeção de construtor quando um tipo expõe apenas um construtor sem parâmetros. Como alternativa, se o aplicativo não usar o Shell, você precisará usar a resolução de dependência explícita.

O contêiner de injeção de dependência pode ser acessado explicitamente em um Handler.MauiContext.Service por meio de sua propriedade Element, que é do tipo IServiceProvider:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        HandlerChanged += OnHandlerChanged;
    }

    void OnHandlerChanged(object sender, EventArgs e)
    {
        BindingContext = Handler.MauiContext.Services.GetService<MainPageViewModel>();
    }
}

Essa abordagem pode ser útil se você precisar resolver uma dependência de um Element, ou de fora do construtor de um Element. Neste exemplo, acessar o contêiner de injeção de dependência no manipulador de eventos HandlerChanged garante que um manipulador tenha sido definido para a página e, portanto, que a propriedade Handler não será null.

Aviso

A propriedade Handler de seu Element pode ser null, portanto, lembre-se de que talvez seja necessário considerar essa situação. Para obter mais informações, consulte o ciclo de vida do manipulador.

Em um modelo de exibição, o contêiner de injeção de dependência pode ser acessado explicitamente por meio da propriedade Handler.MauiContext.Service de Application.Current.MainPage:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel()
    {
        _loggingService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<ILoggingService>();
        _settingsService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<ISettingsService>();
    }
}

Uma desvantagem dessa abordagem é que o modelo de exibição agora tem uma dependência do tipo Application. No entanto, essa desvantagem pode ser eliminada passando um argumento IServiceProvider para o construtor de modelo de exibição. O IServiceProvider é resolvido por meio da resolução automática de dependência sem precisar registrá-lo no contêiner de injeção de dependência. Com essa abordagem, um tipo e sua dependência IServiceProvider podem ser resolvidos automaticamente desde que o tipo seja registrado com o contêiner de injeção de dependência. Em seguida, IServiceProvider pode ser usado para resolução de dependência explícita:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(IServiceProvider serviceProvider)
    {
        _loggingService = serviceProvider.GetService<ILoggingService>();
        _settingsService = serviceProvider.GetService<ISettingsService>();
    }
}

Além disso, uma instância IServiceProvider pode ser acessada em cada plataforma por meio da propriedade IPlatformApplication.Current.Services.

Limitações com recursos XAML

Um cenário comum é registrar uma página com o contêiner de injeção de dependência e usar a resolução automática de dependência para injetá-la no construtor App e defini-la como o valor da propriedade MainPage:

public App(MyFirstAppPage page)
{
    InitializeComponent();
    MainPage = page;
}

No entanto, nesse cenário, se MyFirstAppPage tentar acessar um StaticResource que foi declarado em XAML no dicionário de recursos App, um XamlParseException será lançado com uma mensagem semelhante a Position {row}:{column}. StaticResource not found for key {key}. Isso ocorre porque a página resolvida por meio da injeção de construtor foi criada antes que os recursos XAML no nível do aplicativo tenham sido inicializados.

Uma solução alternativa para esse problema é injetar um IServiceProvider em sua classe App e usá-lo para resolver a página dentro da classe App:

public App(IServiceProvider serviceProvider)
{
    InitializeComponent();
    MainPage = serviceProvider.GetService<MyFirstAppPage>();
}

Essa abordagem força a árvore de objetos XAML a ser criada e inicializada antes que a página seja resolvida.