Привет, Хабр! Сегодня у нас на повестке дня библиотека 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)


  1. VirRus77
    29.10.2024 07:08

    В чем преимущество Autofac от Microsoft.Extensions.DependencyInjection?


    1. NeoNN
      29.10.2024 07:08

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


    1. AgentFire
      29.10.2024 07:08

      NeoNN описал просто набор дополнительного сахара, но там есть и вещи, которые невозможны в принципе в стандартном DI, их туда просто еще не завезли.

      Например, декораторы, или упомянутые интерцепторы.

      Еще там есть очень полезная регистрация по ключу, но в .NET 8 её поспешно добавили, ибо без нее DI был совсем дохленький в плане функционала.

      Из "сахара" там есть возможность указания делегата для резолва конкретного аргумента конструктора (или свойства, неважно), в то время как в стандартном DI нужно будет писать new X(sp.resolve, sp.resolve, sp.resolve....).

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


      1. ilya-chumakov
        29.10.2024 07:08

        Я видел слишком много неудачных примеров использования декораторов/интерсепторов на "не самых простеньких проектах", чтобы считать их наличие плюсом. Скорее хорошо, что в стандартном DI контейнере этого нет.


        1. AgentFire
          29.10.2024 07:08

          Это как говорить, что молоток плохой, ибо есть люди, которые им по голове бьют)

          Да, допустим интерцепторы редко когда нужны, но их наличие ведь не делает библиотеку более плохой, не так ли?


          1. ilya-chumakov
            29.10.2024 07:08

            Неверно. Но раз уж опускаться до аналогий... никто не говорил, что "молоток плохой".


            1. AgentFire
              29.10.2024 07:08

              Да вроде бы именно так вы и сказали: из вашего личного опыта вы считаете наличие некоего функционала минусом. "Наличие" может быть применимо только к пакету, в котором этот функционал находится.


      1. VirRus77
        29.10.2024 07:08

        Так это есть в стандартном: ActivatorUtilities.CreateInstance


        1. AgentFire
          29.10.2024 07:08

          Ах, красота. Но жаль неочевидно ни разу.


  1. alex1t
    29.10.2024 07:08

    А мне одному кажется, что по коду не хватает generic-типов. Видимо везде пропали угловые скобки и их содержимое. Например в самом начале:

    // Регистрируем ConsoleLogger как реализацию ILogger
    builder.RegisterType().As();

    Что и вместо чего регистрируется - только из комментария понятно


    1. 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>();

      Поэтому да, у автора видимо типы отвалились


  1. 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-класс.

    И с учетом того, что статья рекламирует курсы, возникают нехорошие подозрения и об актуальности этих самых курсов ;-)