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 существует три вида жизненного цикла зависимостей:
- Transient - новый объект при создании зависимости
- Scoped - один объект на некоторый скоуп, который в стандартной библиотеке представлен объектом
IServiceProvider. - Singleton - один объект на время работы приложения
В Autofac описание типов жизненного цикла представлены в виде методов расширения при регистрации:
- Singleton представлен методом
.SingleInstance() - Transient представлен методом
.InstancePerDependency() - 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.