Wednesday, November 27, 2013

Использование IoC контейнера Autofac

Эта статья посвящена  IoC (Inversion of  Control) контейнеру Autofac. Это очень интересный IoC контейнер для управления зависимостями. Хотя бы потому, что кроме базового функционала, который присущий во всех IoC контейнерах, разработчики потрудились и написали несколько отдельных библиотек, которые позволяют управлять зависимостями для ASP.NET MVC4/MVC3, WCF Integration, WebApi Integration и WebForm Integration для ASP.NET. Вы  можете это посмотреть на рисунке ниже.

Эта статья не является вводной по IoC контейнерам и Dependency Injection (DI), предполагается, что читатель уже знаком с понятиями IoC container и Dependency Injection (DI). Также этой статье не будет приводить сравнение Autofac с другими контейнерами. В большинстве современных контейнеров для управления зависимостями есть поддержка ASP.NET MVC4/MVC3, WCF, WebApi иначе эти контейнеры окажутся непригодными для использования в более крупных проектах. Поскольку я не буду сейчас останавливаться на вопросах производительности, рассмотрим особенности этого контейнера. Для примера используем ту же реализацию электронной библиотеки, которую Вы можете посмотреть в моих статьях (Основы MVVM и MVVM Part 2). Исходники к статье будут доступны для скачивания по ссылке, приведенной в списке литературы. Чтобы понимать, какие интерфейсы будут использоваться, их описание приведено ниже.
public interface IBook
{
    string Author { get; set; }
    string Title { get; set; }
    DateTime Year { get; set; }
    string SN { get; set; }
    int Count { get; set; }
}
Этот интерфейс предназначен для хранения информации о книге.
public interface ILibraryBookService
{
    void GetData(Action<ObservableCollection<IBook>, Exception> callback);
    IBook FindBook(IBook findBook);
    void CreateNewBook();
    void RemoveBook(IBook book);
}
Интерфейс ILibraryBookService используется для описания классов, которые будут использоваться для поиска книг, байндинга с основной моделью проекта. 
Для реализации этого интерфейса было реализовано два класса: LibraryBookService и HomeLibraryBookServiceПервый реализует электронную библиотеку, например, компьютерного магазина, второй реализует поиск книг по домашней коллекции книг.
public interface IVisitorRepository
{
    List<Visitor> GetAll();
}
Интерфейс IVisitorRepository используется для получения информации о посетителях библиотеки. Реализация этого интерфейса приведена в классе VisitorRepository.
Поскольку для демонстрации примеров использовался WPF/MVVM, нужно привести описание главной модели представления (ViewModel) MainViewModelс помощью которой происходит связывание модели и представления. Реализация MainViewModel выглядит следующим образом:
public class MainViewModel : NotifyModelBase
{
    private readonly ILibraryBookService _libraryDataService;

    public MainViewModel(ILibraryBookService dataService)
    {
        _libraryDataService = dataService;
        _libraryDataService.GetData(
            (items, error) =>
            {
                Books = items;
            });
    }

    #region Public Properties

    public IVisitorRepository VisitorRepository { get; set; }

    public ObservableCollection<IBook> Books { get; set; }

    public void PrintBook(ILibraryBookService libraryBookService)
    {
        if(libraryBookService == null)
            throw new ArgumentException("libraryBookService");
           
        libraryBookService.GetData((items, error) =>
        {
            var books = items.Aggregate(string.Empty, (current, book) => current + (book + Environment.NewLine));
            MessageBox.Show(books);
        });
    }

    private IBook _selectedBook;
    public IBook SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            _selectedBook = value;
            OnPropertyChanged("SelectedBook");
            RemoveBookCommand.RaiseCanExecuteChanged();
        }
    }
    #endregion

    #region Command
    private DelegateCommand _addBookCommand;
    public DelegateCommand AddBookCommand
    {
        get
        {
            return _addBookCommand ?? (_addBookCommand = new DelegateCommand(AddNewBook));
        }
    }

    private void AddNewBook(object args)
    {
        _libraryDataService.CreateNewBook();
    }

    private DelegateCommand _removeBookCommand;
    public DelegateCommand RemoveBookCommand
    {
        get
        {
            return _removeBookCommand ?? (_removeBookCommand = new DelegateCommand(RemoveBook, CanRemoveBook));
        }
    }

    private void RemoveBook(object args)
    {
        Books.Remove(SelectedBook);
    }

    private bool CanRemoveBook(object args)
    {
        if (SelectedBook == null)
            return false;
        var book = _libraryDataService.FindBook(SelectedBook);
        if (book == null)
            return false;

        return true;
    }
    #endregion
}
Для того, чтобы показать, как использовать контейнер Autofac, удалим в App.xaml строчку для запуска формы в Dependency Property (DP)   StartupUri. Чтобы инициализировать IoC контейнер, перед стартом программы необходимо перейти в классы App.xaml.cs и переопределить метод OnStartup.  Затем необходимо написать соответствующую реализацию.
var builder = new ContainerBuilder();
builder.RegisterType<LibraryBook>().As<IBook>();
builder.RegisterType<HomeLibraryBookService>().As<ILibraryBookService>();
builder.RegisterType<LibraryBookService>().As<ILibraryBookService>();
builder.RegisterType<VisitorRepository>().As<IVisitorRepository>();
builder.RegisterType<MainViewModel>().AsSelf();
var container = builder.Build();
var model = container.Resolve<MainViewModel>();
var view = new MainWindow { DataContext = model };
view.Show();
После запуска приложения мы увидим желаемый результат. При использовании контейнера Autofac Вы увидите, что терминология немного отличается от той, которую обычно используют .Net разработчики. У разработчиков Autofac существует термин – Сервис. Сервисом они называют интерфейс, который должен быть реализован компонентом-провайдером и использован компонентом-потребителем (Источник). Вначале это немного непривычно, но к этому можно быстро привыкнуть. С самого начала необходимо создать экземпляр класса ContainerBuiler, который позволяет произвести необходимую привязку Producer-Consumer, если говорить терминами Autofac. С помощью метода этого класса  RegisterType мы делаем привязку интерфейса  с реализацией. Привязка к необходимому интерфейсу осуществляется с помощью метода As<TService >, который имеет несколько перегруженных вариантов. Для того, чтобы зарегистрировать компонент  и указать, что он предоставляет себе, в качестве сервиса используется метод AsSelf(). Последний используется в приведенном примере для регистрации основной модели представления. Когда Вы используете привязку
builder.RegisterType<LibraryBookService>().As<ILibraryBookService>();
Вы можете создать экземпляр нужного класса двумя методами:
container.Resolve<ILibraryBookService>();
но не таким методом:
container.Resolve<LibraryBookService>();
Для того, чтобы были доступны оба способа создания экземпляра класса, нужно использовать метод AsSelf(), упомянутый выше. Вот как будет выглядеть модифицированный вариант:
builder.RegisterType<LibraryBookService>().AsSelf().As<ILibraryBookService>();
Теперь доступны оба метода для регистрации необходимого класса. С помощью метода Build() мы осуществляем необходимое связывание и получаем контейнер, с помощью которого можем получать нужные нам данные; метода Resolve. Autofac способен проверять тип, выбирая соответствующий конструктор, и создавать экземпляр с помощью отражения.  Ниже приведен вариант generic версии и non-generic версии.
builder.RegisterType<LibraryBookService>().As<ILibraryBookService>();
Создание LibraryBookService с помощью рефлексии.           builder.RegisterType(typeof(LibraryBookService)).As(typeof(ILibraryBookService));
Реализация с указанием необходимого типа. Поскольку Autofac используется для управления DI, в приведенной статье будут рассмотрены все три варианта управления зависимостями. А именно Constructor Injection – внедрение зависимостей через конструктор. Это один из самых популярных DI. Существуют еще два менее популярных – это Property Injection и Method Injection.
Пожалуй, стоит упомянуть об некоторых важных особенностях Autofac. Для связывания простых компонентов в Autofac reflection является очень хорошим выбором по умолчанию. Но все становится слишком запутанным, когда логика по созданию компонента выходит за рамки простого вызова конструктора. Для этого в Autofac есть интересная возможность. Он может принимать делегат или лямбда-выражение, которые будут использоваться в качестве составной создателя:
var builder = new ContainerBuilder();
builder.RegisterType<LibraryBook>().As<IBook>();
builder.RegisterType<HomeLibraryBookService>().AsSelf().As<ILibraryBookService>();
builder.RegisterType<VisitorRepository>().As<IVisitorRepository>();
builder.RegisterType<LibraryBookService>().AsSelf().As<ILibraryBookService>();
builder.Register(c => new MainViewModel(c.Resolve<HomeLibraryBookService>()));
var container = builder.Build();
var model = container.Resolve<MainViewModel>();
var view = new MainWindow { DataContext = model };
view.Show();
В приведенном выше примере я использовал для передачи в класс MainViewModel нужную мне реализацию.
builder.Register(c => new MainViewModel(c.Resolve<HomeLibraryBookService>()));
Если у нас конструктор использует несколько простых параметров, то мы можем вызвать метод WithParameters для того, чтобы передать необходимые параметры. Также этот контейнер имеет интересный метод UsingConstructor, который позволяет выбрать один из нескольких доступных конструкторов для конкретного класса. Для демонстрации я добавлю в класс MainViewModel конструктор с параметром типа string. Теперь в приведенном выше коде делаем такую замену:
//builder.Register(c => new MainViewModel(c.Resolve<HomeLibraryBookService>()));
builder.RegisterType<MainViewModel>().UsingConstructor(typeof(string))
    .WithParameter(new NamedParameter("message", "Hello Autofac"));
Теперь мы используем созданный нами второй конструктор и передаем ему необходимый параметр. Autofac предоставляет очень простой и понятный api, необходимый для использования данного контейнера. Мне импонирует возможность Autofac сканировать сборки (Assemblies) для регистрации. Вот как будет выглядеть пример, когда Autofac сам найдет нужное соответствие интерфейсов и реализаций:
var builder = new ContainerBuilder();
builder.RegisterAssemblyTypes(GetType().Assembly).AsSelf().AsImplementedInterfaces();
var container = builder.Build();
var model = container.Resolve<MainViewModel>();
var view = new MainWindow { DataContext = model };
view.Show(); 
Есть несколько методов для того, чтобы облегчить построение общих соглашений:
  • AsImplementedInterfaces () – зарегистрировать тип, а также предоставление всех своих публичных интерфейсов (за исключением IDisposable).
  • AsClosedTypesOf (open) – зарегистрировать типы, присваиваемые в закрытом экземпляре (для закрытых типов privateprotected и т.д. делает привязку).
  • AsSelf () – регистрация сервиса как самого себя.
Разработчики Autofac очень хорошо потрудились, чтобы сделать все как можно проще и понятнее. Хотелось бы затронуть два момента, которые мне понравились. Первый  из них связан с динамическим конфигурированием привязки во время выполнения. Пример:
var builder = new ContainerBuilder();
builder.RegisterType<LibraryBook>().As<IBook>();
builder.RegisterType<HomeLibraryBookService>().AsSelf().As<ILibraryBookService>();
builder.RegisterType<VisitorRepository>().As<IVisitorRepository>();
builder.RegisterType<LibraryBookService>().AsSelf().As<ILibraryBookService>();
builder.Register<ILibraryBookService>((c, p) =>
{
    var accountId = p.Named<string>("accountId");
    if (accountId.StartsWith("9"))
        return new HomeLibraryBookService();
    else
        return new LibraryBookService();
});
builder.Register(c => new MainViewModel(
    c.Resolve<ILibraryBookService>(new NamedParameter("accountId", "9893"))));
var container = builder.Build();
var model = container.Resolve<MainViewModel>();
var view = new MainWindow { DataContext = model };
view.Show();
Как видно из примера, мы можем конфигурировать нужную связку так, как нам необходимо. Второй момент, которого позволяет добиться Autofac – это поддержка модульности. Этот IoC контейнер позволяет разбивать нужную реализацию на модули и инициализировать эти модули отдельно друг от друга. Поскольку приведенный мной пример небольшой, достаточно будет создать один модуль для того, чтобы показать, как это работает. Весь код по настройке сервисов (термин Autofac) был вынесен в отдельный класс LibraryModule.
public class LibraryModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<LibraryBook>().As<IBook>();
        builder.RegisterType<HomeLibraryBookService>().AsSelf().As<ILibraryBookService>();
        builder.RegisterType<VisitorRepository>().As<IVisitorRepository>();
        builder.RegisterType<LibraryBookService>().AsSelf().As<ILibraryBookService>();
        builder.Register<ILibraryBookService>((c, p) =>
        {
            var accountId = p.Named<string>("accountId");
            if (accountId.StartsWith("9"))
                return new HomeLibraryBookService();
            else
                return new LibraryBookService();
        });
        builder.Register(c => new MainViewModel(
            c.Resolve<ILibraryBookService>(new NamedParameter("accountId", "9893"))));
    }
}
Теперь в App.xaml.cs остался только такой код:
var builder = new ContainerBuilder();
builder.RegisterModule<LibraryModule>();
var container = builder.Build();
var model = container.Resolve<MainViewModel>();
var view = new MainWindow { DataContext = model };
view.Show();
Это очень удобно, если поддерживаться дизайна разработки ПО в виде отдельных модулей, а также если в архитектуре проекта есть четкое разделение на программные слои. Как например в проектирование прикладной модели (некоторые источники подают как проблемно-ориентированное проектирование) DDD (англ. Domain driven design).
Autofac также поддерживает настройку сервисов через файл конфигурации. Для такой настройки потребуется отдельная библиотека Autofac.Configuration.dll. Установить ее можно через NuGet консоль.
Install-Package Autofac.Configuration
Вот как будет выглядеть конфигурирование через файл конфигурации:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="autofac" type="Autofac.Configuration.SectionHandler, Autofac.Configuration"/>
  </configSections>
  <autofac defaultAssembly="AutofacDemo">
    <modules>
      <module type="AutofacDemo.Helpers.LibraryModule" />
    </modules>
  </autofac>
</configuration>
Регистрация будет незначительно отличаться одной строкой кода.
var builder = new ContainerBuilder();
builder.RegisterModule(new ConfigurationSettingsReader());
var container = builder.Build();
var model = container.Resolve<MainViewModel>();
var view = new MainWindow { DataContext = model };
view.Show();
Более подробно со всеми параметрами настройки можно ознакомится по приведенной ссылке. Список там небольшой, так что у вас не должно составить большого труда для того, чтобы начать использовать настройку сервиса через файл конфигурации.
Осталось рассмотреть для полноты картины PI (Property Injection) и MI (Method Injection). Начнем, пожалуй, с PI. Это можно сделать двумя способами.
Первый способ: с помощью использования метода ResolveOptional().
builder.Register(c => new MainViewModel(
    c.Resolve<ILibraryBookService>(new NamedParameter("accountId", "9893")))
    {
        VisitorRepository = c.ResolveOptional<IVisitorRepository>()
    });
Второй способ: с помощью метода PropertiesAutowired().
builder.Register(c => new MainViewModel(
    c.Resolve<ILibraryBookService>(new NamedParameter("accountId", "12345")))).PropertiesAutowired();
C MI в контейнере Autofac связана очень интересная особенность. Autofac поддерживает LifetimeEvents (примечание: это также применимо к PI). Это события, которые могут быть подключены на различных этапах жизненного цикла экземпляра. Их всего три, поэтому их легко запомнить:
  • OnActivating
  • OnActivated
  • OnRelease
OnActivating – используется перед использованием компонента. Этот метод позволяет:
1. Переключение с одного экземпляра (instance) на другой или оборачивание его в прокси;
2.     Выполнение property injection и method injection;
3.     Выполнение других задач по инициализации.
OnActivated – вызывается, когда объект полностью инициализирован.
OnRelease – заменяет стандартное поведение очистки для компонента.
Приведем пример, как можно сделать MI.
builder.Register(c =>
    {
        var library = c.Resolve<ILibraryBookService>(new NamedParameter("accountId", "12345"));
        var result = new MainViewModel(
            library);
        result.PrintBook(library);
        return result;
    }).PropertiesAutowired();
И второй способ с помощью LifetimeEvents  OnActivated.
builder.Register(c =>
    new MainViewModel(
    c.Resolve<ILibraryBookService>(new NamedParameter("accountId", "12345"))))
    .PropertiesAutowired()
    .OnActivating(e =>
    {
        var dep = e.Context.Resolve<HomeLibraryBookService>();
        e.Instance.PrintBook(dep);
    });

Итоги
В приведенной статье были рассмотрены примеры использования IoC контейнера AutofacAutofac – это мощный инструмент для управления зависимостями; он легкий для понимания, гибкий и позволяет использовать его в кратчайшие строки. Также замечено, что этот контейнер по скорости выигрывает Unity. Unity – мой любимый IoC контейнер. Но вынужден признать, что по простоте использования и конфигурированию через конфигурационный файл Autofac выигрывает, поэтому на данном этапе этот контейнер заслуживает ту репутацию, которую он имеет. 

Источники: