Привет, Хабр! Сегодня у нас на повестке дня библиотека Autofac — один из самых популярных инструментов для внедрения зависимостей в C#. Разберемся, как она помогает упорядочить код и сделать проект более управляемым.
Установка
Начнём с самого простого — установки Autofac. Просто используйте NuGet:
Install-Package Autofac
Или через .NET CLI:
dotnet add package Autofac
Использование Autofac
Начнём с простого примера, чтобы понять, как работает Autofac. Представим, есть интерфейс ILogger
и его реализация ConsoleLogger
.
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
Теперь создадим сервис, который зависит от ILogger
:
public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger)
{
_logger = logger;
}
public void CreateUser(string username)
{
// Логика создания пользователя
_logger.Log($"User {username} created.");
}
}
Без DI UserService
должен был бы создавать экземпляр ConsoleLogger
самостоятельно, что делает его тесно связанным с конкретной реализацией. С Autofac этого можно избежать!
Создадим контейнер и зарегистрируем наши зависимости:
var builder = new ContainerBuilder();
// Регистрируем ConsoleLogger как реализацию ILogger
builder.RegisterType().As();
// Регистрируем UserService
builder.RegisterType();
// Создаём контейнер
var container = builder.Build();
Теперь можно создать экземпляр UserService
через контейнер:
using (var scope = container.BeginLifetimeScope())
{
var userService = scope.Resolve();
userService.CreateUser("JohnDoe");
}
И всё! Теперь UserService
получает ILogger
от Autofac, и можно легко менять реализации логгера, не затрагивая сам сервис.
Жизненные циклы объектов
Одна из самых крутых фич в Autofac – это управление жизненным циклом объектов. Рассмотрим основные жизненные циклы, которые имеет Autofac:
Transient (по дефолту): Каждый раз создаётся новый экземпляр.
Singleton: Создаётся только один экземпляр на весь контейнер.
InstancePerLifetimeScope: Один экземпляр на один жизненный цикл.
Пример использования жизненных циклов:
// Singleton: один экземпляр для всего приложения
builder.RegisterType().As().SingleInstance();
// InstancePerLifetimeScope: один экземпляр на scope
builder.RegisterType().As().InstancePerLifetimeScope();
// Transient: новый экземпляр каждый раз (по умолчанию)
builder.RegisterType().As();
Модули Autofac
Когда есть большой проект, регистрация всех зависимостей в одном месте часто становится неудобной. Для решения этой проблемы используются модули. Модули позволяют организовать регистрацию зависимостей в логические блоки.
Создание модуля:
public class LoggingModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
}
}
Подключение модулей:
var builder = new ContainerBuilder();
builder.RegisterModule(new LoggingModule());
var container = builder.Build();
Теперь все зависимости, зарегистрированные в LoggingModule
, будут добавлены в контейнер.
Внедрение зависимостей через конструктор, свойства и методы
Autofac поддерживает несколько способов внедрения зависимостей: через конструктор, свойства и методы.
Внедрение через конструктор
Это самый распространённый способ. Как мы видели ранее, зависимости передаются через параметры конструктора.
public class OrderService
{
private readonly ILogger _logger;
private readonly IEmailService _emailService;
public OrderService(ILogger logger, IEmailService emailService)
{
_logger = logger;
_emailService = emailService;
}
public void PlaceOrder(Order order)
{
// Логика размещения заказа
_logger.Log("Order placed.");
_emailService.SendOrderConfirmation(order);
}
}
Внедрение через свойства
Иногда хорошо внедрять зависимости через свойства, особенно если они не всегда необходимы.
public class NotificationService
{
public ILogger Logger { get; set; }
public void Notify(string message)
{
Logger?.Log(message);
// Логика уведомления
}
}
Для этого нужно настроить Autofac:
builder.RegisterType()
.PropertiesAutowired();
Внедрение через методы
Autofac также позволяет внедрять зависимости через метод.
public class PaymentService
{
private ILogger _logger;
public void Initialize(ILogger logger)
{
_logger = logger;
}
public void ProcessPayment(Payment payment)
{
_logger.Log("Processing payment.");
// Логика оплаты
}
}
Настройка Autofac:
builder.RegisterType()
.OnActivated(e => e.Instance.Initialize(e.Context.Resolve()));
Обработка коллекций зависимостей
Иногда нужно внедрить несколько реализаций одного интерфейса.
Предположим, есть несколько реализаций ILogger
:
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"ConsoleLogger: {message}");
}
public class FileLogger : ILogger
{
public void Log(string message) => File.AppendAllText("log.txt", $"FileLogger: {message}\n");
}
И нужно использовать все логгеры в сервисе:
public class CompositeLogger : ILogger
{
private readonly IEnumerable _loggers;
public CompositeLogger(IEnumerable loggers)
{
_loggers = loggers;
}
public void Log(string message)
{
foreach (var logger in _loggers)
{
logger.Log(message);
}
}
}
Регистрация зависимостей:
builder.RegisterType().As();
builder.RegisterType().As();
builder.RegisterType().As();
Теперь, когда идет запрос наCompositeLogger
, Autofac предоставит все зарегистрированные ILogger
реализации.
Обработка ленивых зависимостей
Иногда вам не нужно создавать зависимость сразу при создании объекта. Вместо этого можно создать её только тогда, когда она действительно понадобится. Для этого можно использовать Lazy
.
public class ReportService
{
private readonly Lazy _dataFetcher;
public ReportService(Lazy dataFetcher)
{
_dataFetcher = dataFetcher;
}
public void GenerateReport()
{
var data = _dataFetcher.Value.FetchData();
// Логика генерации отчета
}
}
Autofac автоматически поддерживает Lazy
, так что доп. настройки не требуется.
Внедрение зависимостей через фабрики
Иногда нужно создать объект с определёнными параметрами или выполнить сложную инициализацию. Для этого можно юзать фабрики.
public class Widget
{
public string Name { get; set; }
public int Size { get; set; }
}
public class WidgetFactory
{
private readonly IComponentContext _context;
public WidgetFactory(IComponentContext context)
{
_context = context;
}
public Widget Create(string name, int size)
{
return new Widget
{
Name = name,
Size = size
};
}
}
Регистрация фабрики:
builder.RegisterType();
Использование фабрики:
using (var scope = container.BeginLifetimeScope())
{
var factory = scope.Resolve();
var widget = factory.Create("Gadget", 10);
}
Интерцепторы и аспектно-ориентированное программирование
Autofac поддерживает перехватчики!
public interface ICalculator
{
int Add(int a, int b);
}
public class Calculator : ICalculator
{
public int Add(int a, int b) => a + b;
}
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine($"Calling method {invocation.Method.Name}");
invocation.Proceed();
Console.WriteLine($"Method {invocation.Method.Name} completed");
}
}
Регистрация интерцептора:
builder.RegisterType();
builder.RegisterType()
.As()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(LoggingInterceptor));
Использование:
using (var scope = container.BeginLifetimeScope())
{
var calculator = scope.Resolve();
var result = calculator.Add(2, 3);
// Вывод:
// Calling method Add
// Method Add completed
}
Пример использования
Приведу пример использования Autofac в проекте на ASP.NET Core. Представим, что есть веб-приложение, структурированное на слои сервисов и репозиториев. Для управления зависимостями было принято решение использовать Autofac.
В приложении есть интерфейсы IUserRepository
и IUserService
с соответствующими реализациями UserRepository
и UserService
. Контроллер UserController
зависит от IUserService
. Начнем с установки необходимых пакетов через NuGet:
Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection
После установки пакетов переходим к настройке контейнера Autofac. В файле Program.cs
настраиваем хостинг-приложения, указывая использование AutofacServiceProviderFactory
:
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
});
}
В классе Startup
мы конфигурируем сервисы и контейнер Autofac. В методе ConfigureServices
добавляем стандартные сервисы MVC, а в методе ConfigureContainer
регистрируем зависимости:
using Autofac;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType()
.As()
.InstancePerLifetimeScope();
builder.RegisterType()
.As()
.InstancePerLifetimeScope();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Реализация интерфейсов выглядит следующим образом. Интерфейс IUserRepository
определяет метод для получения пользователя по идентификатору, а его реализация UserRepository
содержит логику доступа к данным:
public interface IUserRepository
{
User GetUserById(int id);
}
public class UserRepository : IUserRepository
{
public User GetUserById(int id)
{
// Логика доступа к данным
return new User { Id = id, Name = "John Doe" };
}
}
Сервисный слой представлен интерфейсом IUserService
и его реализацией UserService
, которая зависит от IUserRepository
:
public interface IUserService
{
User GetUser(int id);
}
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public User GetUser(int id)
{
return _userRepository.GetUserById(id);
}
}
А контроллер UserController
уже использует IUserService
для обработки HTTP-запросов:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var user = _userService.GetUser(id);
if (user == null)
return NotFound();
return Ok(user);
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
В этом примере Autofac отвечает за создание и управление жизненным циклом объектов. Когда UserController
запрашивает IUserService
, Autofac автоматически создает экземпляр UserService
, внедряя в него IUserRepository
.
Заключение
Если хотите более подробно изучить библиотеку и узнать все нюансы, обязательно загляните в официальную документацию Autofac.
Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.
А 12 ноября пройдет открытый урок, посвященный теме поведенческих шаблонов проектирования в C#. Мы рассмотрим, как, используя язык и его особенности, реализовать поведенческие шаблоны и в каких случаях они будут полезны. Записаться на урок можно на странице курса «C# Developer. Professional».
Комментарии (12)
alex1t
29.10.2024 07:08А мне одному кажется, что по коду не хватает generic-типов. Видимо везде пропали угловые скобки и их содержимое. Например в самом начале:
// Регистрируем ConsoleLogger как реализацию ILogger builder.RegisterType().As();
Что и вместо чего регистрируется - только из комментария понятно
comradeleet
29.10.2024 07:08Я сначала подумал что тут магия какая-то невероятная и впал в шок от настолько неявной привязки)
Но залез в документацию, там:// Create your builder. var builder = new ContainerBuilder(); // Usually you're only interested in exposing the type // via its interface: builder.RegisterType<SomeType>().As<IService>(); // However, if you want BOTH services (not as common) // you can say so: builder.RegisterType<SomeType>().AsSelf().As<IService>();
Поэтому да, у автора видимо типы отвалились
mvv-rus
29.10.2024 07:08В статье мне не понравилось, что она написана как будто десять лет назад, когда в .NET Framework штатных контейнеров сервисов не было, а для использования сторонних контейнеров для внедрения зависимостей в MVC приходилось делать лишние телодвижения (кому интересно - могут найтим книжку А.Фримана по MVC4 - они там подробно описаны).
Но сейчас-то мы живем на десять лет позже, когда в .NET давно есть стандартный контейнер зависимостей, который много где используется, а например ASP.NET Core и Background services без контейнера зависимостей просто работать не могут. И - когда вместо стандартного контейнера вполне можно подключить сторонний, чтобы им пользовались все компоненты .NET котрым этот контейнер нужен.
Вместо этого автор сосредоточился на специфических для Autofac методах, вместо того, чтобы использовать стандартный для любого контейнера IServiceProvider и всю остальную обвязку из Microsoft.Extensions.DependencyInjection - одноообразно которая работает и со встроенным контейнером, и со сторонними. То есть автор показывает примеры работы, специфичные именно для конкретного стороннего контейнера, вместо того, чтобы указать универсальные примеры.
В частности, показывая конфигурирование контейнера сервисов в Startup-классе, автор зачем-то сосредоточился на специфичном для Autofac конфигурировании внутри ConfigureContainer, тогда как все то же самое вполне делается в не зависящем от контейнера и куда более привычном ConfigureServices (это - не говоря о том, что вообще странно в 2024 году приводить примеры настройки ASP.NET Core на устаревшем несколько лет назад шаблоне приложения, где используется Startup-класс.
И с учетом того, что статья рекламирует курсы, возникают нехорошие подозрения и об актуальности этих самых курсов ;-)
VirRus77
В чем преимущество Autofac от Microsoft.Extensions.DependencyInjection?
NeoNN
Модули, возможность подтянуть типы автоматом через рефлексию, более тонкая настройка резолвов. Но обычно и стандартного хватает.
AgentFire
NeoNN описал просто набор дополнительного сахара, но там есть и вещи, которые невозможны в принципе в стандартном DI, их туда просто еще не завезли.
Например, декораторы, или упомянутые интерцепторы.
Еще там есть очень полезная регистрация по ключу, но в .NET 8 её поспешно добавили, ибо без нее DI был совсем дохленький в плане функционала.
Из "сахара" там есть возможность указания делегата для резолва конкретного аргумента конструктора (или свойства, неважно), в то время как в стандартном DI нужно будет писать new X(sp.resolve, sp.resolve, sp.resolve....).
Короче, Autofac в целом делает жизнь намного проще, если у вас проект не самый простенький.
ilya-chumakov
Я видел слишком много неудачных примеров использования декораторов/интерсепторов на "не самых простеньких проектах", чтобы считать их наличие плюсом. Скорее хорошо, что в стандартном DI контейнере этого нет.
AgentFire
Это как говорить, что молоток плохой, ибо есть люди, которые им по голове бьют)
Да, допустим интерцепторы редко когда нужны, но их наличие ведь не делает библиотеку более плохой, не так ли?
ilya-chumakov
Неверно. Но раз уж опускаться до аналогий... никто не говорил, что "молоток плохой".
AgentFire
Да вроде бы именно так вы и сказали: из вашего личного опыта вы считаете наличие некоего функционала минусом. "Наличие" может быть применимо только к пакету, в котором этот функционал находится.
VirRus77
Так это есть в стандартном: ActivatorUtilities.CreateInstance
AgentFire
Ах, красота. Но жаль неочевидно ни разу.