DI-контейнер Autofac. Часть 1

DI-контейнер Autofac. Часть 1

Дмитрий Бахтенков

Введение

Autofac - это популярная и мощная библиотека для внедрения зависимостей в C#, которая позволяет легко и гибко управлять жизненным циклом объектов и разрешать зависимости между компонентами. В этой статье мы разберём основные концепции и принципы работы autofac. Если вы плохо знакомы с концепцией Dependency Injection - у меня есть другая статья с описанием этого подхода.

Преимущества и недостатки

Среди преимуществ autofac можно выделить следующие:

  • Autofac поддерживает различные виды внедрения зависимостей: через конструктор, свойства или методы.
  • Autofac позволяет регистрировать и разрешать зависимости с помощью атрибутов, метаданных, фабрик или перехватчиков (Interceptors).
  • Autofac обладает высокой гибкостью и расширяемостью, позволяя создавать модули, теги и области для управления жизненным циклом объектов.

Среди недостатков autofac можно отметить следующие:

  • Autofac может быть менее производительным, чем некоторые другие библиотеки для внедрения зависимостей. Подробнее об этом можно узнать из бенчмарка DI-контейнеров - https://github.com/danielpalme/IocPerformance
  • Autofac может быть избыточным или сложным. Если вы разрабатываете простое приложение, в котором достаточно функций стандартного контейнера от Microsoft - используйте его.
  • Autofac может приводить к ошибкам или утечкам памяти, если неправильно управлять жизненным циклом объектов или областями видимости.

Основы работы с Autofac

Установка и подключение в Asp.Net Core

Для начала нужно создать проект asp.net Core Web Api.

Для того, чтобы подключить Autofac к проекту, необходимо установить соответствующие библиотеки через NuGet:

  • Autofac
  • Autofac.Extensions.DependencyInjection

В файле program.cs мы используем builder, который “строит” наше приложение. Этот объект по умолчанию использует контейнер Microsoft.Extensions.DependencyInjection. Нам необходимо указать использование другого контейнера - для этого используем метод UseServiceProviderFactory:

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

Теперь с помощью метода ConfigureContainer мы можем добавлять в наш контейнер различные сервисы:

builder.Host.ConfigureContainer<ContainerBuilder>((containerBuilder) =>
{
    containerBuilder.Register<Dependency>().As<IDependency>();
});

Такой подход очень похож на регистрацию зависимостей в стандартном DI-контейнере от Microsoft. При этом у Autofac есть много других полезных фич, которые мы рассмотрим дальше.

Регистрация зависимостей

Autofac предоставляет множество способов регистрации компонентов. Все они описаны в документации к этой библиотеке - Registration Concepts — Autofac 7.0.0 documentation, а мы рассмотрим самые основные.

Стандартная регистрация - почти то же самое, что и в контейнере от Microsoft: используя метод RegisterType<>(), с указанием типа жизненного цикла компонента и, при необходимости, интерфейса. Например:

builder.RegiserType<Dependency>().As<IDependency>()

Регистрация единственного инстанса класса - мы можем явно создать какой-то объект, проинициализировать его и зарегистрировать в контейнере. Получится некоторый синглтон-объект, что может быть полезно в случае реализации некоторого сетевого клиента, например к Telegram Api:

var client = new TelegramClient(Config.Token);
builder.RegisterInstance(client);

После такой регистрации мы сможем использовать внедрение через конструктор, и при выполнении приложения буде получать ссылку на конкретный объект client.

Регистрация с помощью lambda-выражений - можно написать свою логику регистрации компонентов. Такое может пригодиться в тех случаях, когда у нас есть несколько реализаций интерфейса, и мы должны подставлять конкретную в зависимости от внешних условий. Допустим, у нас есть некоторый IDateTimeService, его стандартная реализация и мок со статическим значением:

public interface IDateTimeService
{
    public DateTime UtcNow();
}

public class DateTimeService : IDateTimeService
{
    public DateTime UtcNow()
    {
        return DateTime.UtcNow;
    }
}

public class StaticDateTimeService : IDateTimeService
{
    private readonly DateTime _staticTime;

    public StaticDateTimeService(DateTime staticTime)
    {
        _staticTime = staticTime;
    }

    public DateTime UtcNow()
    {
        return _staticTime;
    }
}

Нам необходимо возвращать ту или иную реализацию в зависимости от окружения, в котором запущено приложение:

builder.Host.ConfigureContainer<ContainerBuilder>((containerBuilder) =>
{
    containerBuilder.Register<IDateTimeService>(context =>
    {
        if (builder.Environment.IsEnvironment("TEST"))
        {
            return new StaticDateTimeService(new DateTime(2011, 11, 11));
        }

        return new DateTimeService();
    });
});

Попробуем добавить в приложение контроллер и запустить API в разных окружениях.

  • Стандартное:
  • Тестовое:

Регистрация generic-компонентов. Классы с generic-параметрами регистрируются с помощью метода RegisterGeneric(), например:

builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)) 

Жизненный цикл зависимостей

В стандартном контейнере от Microsoft существует три вида жизненного цикла зависимостей:

  1. Transient - новый объект при создании зависимости
  2. Scoped - один объект на некоторый скоуп, который в стандартной библиотеке представлен объектом IServiceProvider.
  3. Singleton - один объект на время работы приложения

В Autofac описание типов жизненного цикла представлены в виде методов расширения при регистрации:

  1. Singleton представлен методом .SingleInstance()
  2. Transient представлен методом .InstancePerDependency()
  3. Scoped представлен методом .InstancePerLifetimeScope()

Помимо стандартных вариантов жизненного цикла Autofac предоставляет расширенные варианты, которые могут вести себя по-разному в иерархии LifetimeScope.

LifetimeScope

Объект LifetimeScope - это по сути контейнер, который содержит информацию о всех зарегистрированных зависимостях и предоставляет их, по аналогии с IServiceProvider из контейнера Microsoft.

Сравним способы получения сервисов из скоупа в этих библиотеках.

Microsoft DI:

using (var scope = _serviceProvider.CreateScope())
{
    var service = scope.ServiceProvider.GetRequiredService<IService>();
}

Autofac:

using (var scope = _lifetimeScope.BeginLifetimeScope())
{
    var service = scope.Resolve<IService>();
}

Используя метод _lifetimeScope.BeginLifetimeScope мы создаём новый скоуп на основе уже существующего _lifetimeScope. Это значит, что уже созданные зависимости в родительском скоупе попадут в дочерний, если у них тип жизненного цикла - Singleton и при некоторых других типах. Таким образом мы можем строить иерархию скоупов.

Такой подход может использоваться, например, в реализации логики с множеством потоков, когда нам необходимо, чтобы каждый поток работал с изолированным контейнером.

Скоупы можно помечать различными тегами, чтобы сохранять созданные в родительском LifetimeScope зависимости для дочерних. Для этого используется метод расширения InstancePerMatchingLifetimeScope():

builder.RegisterType<UserService>().InstancePerMatchingLifetimeScope("transaction");

Пример кода, который использует теги для скоупов:

using (var parentScope = _lifetimeScope.BeginLifetimeScope("transaction"))
{
    parentService = parentScope.Resolve<UserService>();

    using (var childScope = parentScope.BeginLifetimeScope())
    {
        childService = childScope.Resolve<UserService>();
        var isEqual = ReferenceEquals(parentService, childService);
    // isEqual == true
    }
} 

Как уже было сказано выше, такой вид жизненного цикла может пригодиться для работы со множеством потоков.

Модули

Как уже было сказано выше, зависимости регистрируются в методе ConfigureContainer, в файле Program.cs. Однако при большом количестве зависимостей, распределённых по разным папкам, этот метод будет очень огромный и нечитаемый. А если над проектом работают несколько разработчиков, то при неаккуратном добавлении зависимостей могут возникать конфликты в гит - решаемые, но неприятные.

Для того, чтобы разбить регистрацию зависимостей по файлам, в Autofac можно использовать модули. Модуль - это некоторый класс, который наследуется от класса Module и отвечает за регистрацию компонентов. Основное преимущество такого подхода - возможность распределить регистрацию зависимостей между доменными группами. Как пример рассмотрим платформу для блога. У нас есть пользователи и посты. Примерная структура проекта:

В папке Users у нас будут различные сервисы, репозитории и команды, а также класс UserModule, который регистрирует всё это в контейнере Autofac:

public class UserModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<UserRepository>().As<IRepository<User>>().InstancePerLifetimeScope();
        builder.RegisterType<UserService>().InstancePerLifetimeScope();
    }
}

В папке Posts у нас также находятся различные сервисы и репозитории, и PostsModule для регистрации всего, что связано с постами.

public class PostsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<PostRepository>().As<IRepository<Post>>().InstancePerLifetimeScope();
        builder.RegisterType<PostService>().InstancePerLifetimeScope();
    }
}

А в файле Program.cs мы просто укажем эти модули:

builder.Host.ConfigureContainer<ContainerBuilder>((containerBuilder) =>
{
    containerBuilder.RegisterModule<UserModule>();
    containerBuilder.RegisterModule<PostsModule>();
});

или же настроим автоматическое обнаружение модулей в текущей сборке C#:

builder.Host.ConfigureContainer<ContainerBuilder>((containerBuilder) =>
{
    containerBuilder.RegisterAssemblyModules(Assembly.GetExecutingAssembly());
});

Реализуем примитивные UserService и UserController, чтобы посмотреть, как работает DI-контейнер:

UserService:

public class UserService
{
    private readonly IRepository<User> _repository;

    public UserService(IRepository<User> repository)
    {
        _repository = repository;
    }

    public async Task<List<User>> Get()
    {
        return await _repository.GetAll();
    }
}

UserController:

[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly UserService _userService;

    public UserController(UserService userService)
    {
        _userService = userService;
    }

    [HttpGet]
    public async Task<List<Models.User>> Get()
    {
        return await _userService.Get();
    }
}

В отладке можно заметить, что полю _userService присвоилась необходимая реализация, в которую пробросилось поле _repository:

Собственно, модули открывают нам путь к автоматической регистрации зависимостей, что особенно удобно, когда в решении множество проектов - мы просто перечисляем наименования сборок, и модули подхватываются автоматически.

Заключение

В этой статье мы рассмотрели базовое использование контейнера Autofac. Разобрались с регистрацией компонентов, жизненным циклом зависимостей и модулями.

Код доступен на гитхабе.

С вами был Flex Code.

Report Page