Управление зависимостями с Autofac в C#
Привет, Хабр! Сегодня у нас на повестке дня библиотека 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».
Habrahabr.ru прочитано 12148 раз