ASP.NET MVC Урок 2. Dependency Injection

в 18:56, , рубрики: .net, ASP, asp.net mvc, autofac, Ninject, unity, метки: , , , , ,

Цель урока: Изучение DI (Dependency Injection). Пример на Ninject и Unity (Autofac, Winsor).

Во многих случаях, один и тот же экземпляр класса используется в вашем приложении в разных модулях. Простым способом реализации является применение шаблона Одиночка (Singleton).

Но рассмотрим эту ситуацию с другой стороны. Так как данный объект создается при первом обращении к нему, мы не можем контролировать его время жизни. При модульном тестировании (unit-test) нет необходимости использовать этот объект (или это может быть невозможно). Чтобы избежать этого, мы не напрямую вызываем объект, а через интерфейс. И реальный экземпляр класса, и экземпляр-заглушка для тестирования будут реализовывать этот интерфейс. А логику создания мы поручаем DI-контейнеру.


Например, до использования сервиса. Опишем пару классов, интерфейс IWeapon с методом Kill, два класса реализации Bazuka и Sword, и класс Warrior, который пользуется оружием:

  public interface IWeapon
    {
        void Kill();
    }
    public class Bazuka : IWeapon
    {
        public void Kill()
        {
            Console.WriteLine("BIG BADABUM!");
        }
    }
    public class Sword : IWeapon
    {
        public void Kill()
        {
            Console.WriteLine("Chuk-chuck");
        }
    }
    public class Warrior
    {
        readonly IWeapon Weapon;

        public Warrior(IWeapon weapon)
        {
            this.Weapon = weapon;
        }

        public void Kill()
        {
            Weapon.Kill();
        }
    }

Используем это:

    class Program
    {
        static void Main(string[] args)
        {
            Warrior warrior = new Warrior(new Bazuka());
            warrior.Kill();
            Console.ReadLine();
        }
    }

Читаем между строк. Создаем воина и даем ему базуку, он идет и убивает. В консоли получаем:

BIG BADABUM!

Заметим, что у нас нет проверки на null в строке

Weapon.Kill();

Что здесь некоректно? Воин не знает, есть ли у него оружие, и выдачей оружия занимается не отдельный модуль, а главная программа.
Суть DI – поручить выдачу оружия другому модулю.

Подключаем Ninject:

Install-Package Ninject

Создаем модуль, который занимается выдачей оружия:

    public class WeaponNinjectModule : NinjectModule
    {
        public override void Load()
        {
            this.Bind<IWeapon>().To<Sword>();
        }
    }

Что буквально значит: «если попросят оружие – то выдайте мечи».
Создаем «сервис-локатор» и пользуемся оружием:

    class Program
    {
        public static IKernel AppKernel;

        static void Main(string[] args)
        {
            AppKernel = new StandardKernel(new WeaponNinjectModule());
            
            var warrior = AppKernel.Get<Warrior>();

            warrior.Kill();
            
            Console.ReadLine();
        }
    }

Как видно, объект warrior мы создаем не с помощью конструкции new, а через AppKernel.Get<>(). При создании AppKernel, мы передаем в качестве конструктора модуль, отвечающий за выдачу оружия (в данном случае это меч). Любой объект, который мы пытаемся получить через AppKernel.Get, будет (по мере возможности) проинициализирован, если существуют модули, которые знают, как это делать.

Другой момент применения, когда объект Warrior не берет с собой оружие каждый раз, а при не обнаружении оного обращается к сервису локатору и получает его:

  public class OtherWarrior
    {
        private IWeapon _weapon; 

        public IWeapon Weapon
        {
            get
            {
                if (_weapon == null)
                {
                    _weapon = Program.AppKernel.Get<IWeapon>();
                }
                return _weapon;
            }
        }

        public void Kill()
        {
            Weapon.Kill();
        }
    }

Исполняем:

      var otherWarrior = new OtherWarrior();
      otherWarrior.Kill();

Наш воин получает оружие по прямым поставкам – супер!

В Ninject есть еще одна очень хорошая деталь. Если свойство (public property) помечено [Inject], то при создании класса через AppKernel.Get<>() – поле инициализуется сервисом-локатором:

    public class AnotherWarrior
    {
        [Inject]
        public IWeapon Weapon { get; set; }

        public void Kill()
        {
            Weapon.Kill();
        }
    }

    var anotherWarrior = AppKernel.Get<AnotherWarrior>();
    anotherWarrior.Kill();
Unity

Абсолютно всё то же:

  • Установка
    Install-Package Unity
    

  • Инициализация сервиса локатора (Container)
    Container = new UnityContainer();
    

  • Регистрация типа
    Container.RegisterType(typeof(IWeapon), typeof(Bazuka));
    

  • Получение объекта и использование:
    var warrior = Container.Resolve<Warrior>();
    warrior.Kill();
    
  • Кроме того, у Unity есть класс-одиночка (Singleton) ServiceLocator, который регистрирует контейнер и позволяет получить доступ к сервисам из любого места.
    var serviceProvider = new UnityServiceLocator(Container);
    ServiceLocator.SetLocatorProvider(() => serviceProvider);
    

  • Хитрый OtherWarrior теперь так получает оружие:
     public class OtherWarrior
        {
            private IWeapon _weapon; 
    
            public IWeapon Weapon
            {
                get
                {
                    if (_weapon == null)
                    {
                        _weapon = ServiceLocator.Current.GetInstance<IWeapon>();
                    }
                    return _weapon;
                }
            }
    
            public void Kill()
            {
                Weapon.Kill();
            }
    }
    
Autofac

Так же, собственно, всё и происходит:

  • Установка
    Install-Package Autofac
    
  • Инициализация строителя сервиса-локатора (ContainerBuilder) – нет-нет, это еще не сам контейнер, это — как модули
    var builder = new ContainerBuilder();
    
  • Регистрация типов. Надо зарегистрировать все необходимые классы, потому что создание экземпляров незарегистрированных классов тут не реализован.

    builder.RegisterType<Bazuka>();
    builder.RegisterType<Warrior>();
    builder.Register<IWeapon>(x => x.Resolve<Bazuka>());
    

  • Создание сервиса локатора (Container)
    var container = builder.Build();
    

  • Получение объекта и использование:
    var warrior = container.Resolve<Warrior>();
    warrior.Kill();
    

Castle Windsor

  • Установка
    Install-Package Castle.Windsor
    

  • Инициализация сервиса-локатора
    var container = new WindsorContainer();
    

  • Регистрация типов. Аналогична как и в Autofac.
    container.Register(Component.For<IWeapon>().ImplementedBy<Bazuka>(),
    Component.For<Warrior>().ImplementedBy<Warrior>());
    

  • Получение объекта и использование:
    var warrior = container.Resolve<Warrior>();
    warrior.Kill();
    

Маленький подитог

На самом деле, реализации Dependency Injection не сильно, но всё же отличаются. Некоторые поддерживают инициализацию в Web.config (App.config) файлах. Некоторые, задают правила для инициализации, как мы сейчас посмотрим на расширении Ninject для asp.net mvc – это касается инициализации сервиса-локатора как генератора общих объектов, так и отдельно для каждого потока или web-запросе.

Объекты областей (Ninject)

В Ninject можно задать несколько способов инициализации получения объекта из класса. Если мы работаем в различных контекстах (например, в разных потоках (Thread)), то объекты должны быть использованы разные. Тем самым, поддерживается масштабируемость и гибкость приложения.

Область Метод связывания Объяснение
Временный .InTransientScope() Объект класса будет создаваться по каждому требованию (метод по умолчанию).
Одиночка .InSingletonScope() Объект класса будет создан один раз и будет использоваться повторно.
Поток .InThreadScope() Один объект на поток.
Запрос .InRequestScope() Один объект будет на каждый web-запрос
Lifetime Manager в Unity

В Unity для задачи правил инициализации используется реализация абстрактного класса LifetimeManager.
Происходит это так:

 _container.RegisterType<DbContext, SavecashTravelContext>(new PerRequestLifetimeManager());

Где PerRequestLifetimeManager – это реализация LifetimeManager:

public class PerRequestLifetimeManager : LifetimeManager
    {
        /// <summary>
        /// Key to store data
        /// </summary>
        private readonly string _key = String.Format("SingletonPerRequest{0}", Guid.NewGuid());

        /// <summary>
        /// Retrieve a value from the backing store associated with this Lifetime policy.
        /// </summary>
        /// <returns>
        /// the object desired, or null if no such object is currently stored.
        /// </returns>
        public override object GetValue()
        {
            if (HttpContext.Current != null && HttpContext.Current.Items.Contains(_key))
                return HttpContext.Current.Items[_key];
            return null;
        }

        /// <summary>
        /// Stores the given value into backing store for retrieval later.
        /// </summary>
        /// <param name="newValue">The object being stored.</param>
        public override void SetValue(object newValue)
        {
            if (HttpContext.Current != null)
                HttpContext.Current.Items[_key] = newValue;
        }

        /// <summary>
        /// Remove the given object from backing store.
        /// </summary>
        public override void RemoveValue()
        {
            if (HttpContext.Current != null && HttpContext.Current.Items.Contains(_key))
                HttpContext.Current.Items.Remove(_key);
        }
    }

Суть. Все объекты хранятся в HttpContext.Current.Items[_key] и выдаются только, если уже находятся в том же контексте (HttpContext.Current). В ином случае, создается новый объект. Если текущий контекст (HttpContext.Current) в области кода не существует (используем такой LifetimeManager в консольном приложении или в отдельном потоке) – то данный контейнер не будет работать.

Использование Ninject в asp.net mvc

Устанавливаем Ninject в среду asp.net mvc. Отдельно создаем свой проект LessonProject, создадим там HomeController с методом и view Index. (/Contollers/HomeController.cs):

public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }

И (/Views/Home/Index.cshtml):

@{
    ViewBag.Title = "LessonProject";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>LessonProject</h2>

Запускаем – работает.

Примечание: В дальнейшем мы будем переносить этот проект в последующие уроки.

Теперь установим модуль Ninject и Ninject.MVC3 для этого проекта.

Install-Package Ninject.MVC3

Добавляем класс в папку App_Start (/App_Start/NinjectWebCommon.cs):


[assembly: WebActivator.PreApplicationStartMethod(typeof(LessonProject.App_Start.NinjectWebCommon), "Start")]
[assembly: WebActivator.ApplicationShutdownMethodAttribute(typeof(LessonProject.App_Start.NinjectWebCommon), "Stop")]
namespace LessonProject.App_Start
{
    using System;
    using System.Web;

    using Microsoft.Web.Infrastructure.DynamicModuleHelper;

    using Ninject;
    using Ninject.Web.Common;

    public static class NinjectWebCommon 
    {
        private static readonly Bootstrapper bootstrapper = new Bootstrapper();

        /// <summary>
        /// Starts the application
        /// </summary>
        public static void Start() 
        {
            DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
            DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
            bootstrapper.Initialize(CreateKernel);
        }
        
        /// <summary>
        /// Stops the application.
        /// </summary>
        public static void Stop()
        {
            bootstrapper.ShutDown();
        }
        
        /// <summary>
        /// Creates the kernel that will manage your application.
        /// </summary>
        /// <returns>The created kernel.</returns>
        private static IKernel CreateKernel()
        {
            var kernel = new StandardKernel();
            kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
            kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
            
            RegisterServices(kernel);
            return kernel;
        }

        /// <summary>
        /// Load your modules or register your services here!
        /// </summary>
        /// <param name="kernel">The kernel.</param>
        private static void RegisterServices(IKernel kernel)
        {
        }        
    }
}

В RegisterServices мы добавляем инициализацию своих сервисов. Для начала добавим шутливый IWeapon, а в дальнейшем еще будем возвращаться к этому методу для регистрации других сервисов:

  public interface IWeapon
    {
        string Kill();
    }
…
    public class Bazuka : IWeapon
    {
        public string Kill()
        {
            return "BIG BADABUM!";
        }
    }
…
    private static void RegisterServices(IKernel kernel)
    {
kernel.Bind<IWeapon>().To<Bazuka>();
    }

В контроллере используем атрибут [Inject]:

  public class HomeController : Controller
    {
        [Inject]
        public IWeapon weapon { get; set; }

        public ActionResult Index()
        {
            return View(weapon);
        }
    }

Изменяем View:

@model LessonProject.Models.IWeapon
@{
    ViewBag.Title = "LessonProject";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>LessonProject</h2>

<p>
@Model.Kill()
</p>

На выходе получаем:
ASP.NET MVC Урок 2. Dependency Injection

Ninject использует WebActivator:

  • регистрирует свои модули OnePerRequestHttpModule и NinjectHttpModule
  • создает StandartKernel
  • инициализирует наши сервисы.
DependencyResolver

В asp.net mvc3 появился класс DependencyResolver. Этот класс обеспечивает получение экземпляра сервиса. Наши зарегистрированные сервисы (и даже используемый DI-контейнер) мы также можем получить посредством этого класса.

    public class HomeController : Controller
    {
        private IWeapon weapon { get; set; }

        public HomeController()
        {
            weapon = DependencyResolver.Current.GetService<IWeapon>();
        }

        public ActionResult Index()
        {
            return View(weapon);
        }
    }

Итог

Использование DI-контейнеров в современных приложениях необходимо, чтобы избавиться от сильной связности кода, и для легкого доступа из любой его части к сервисам. Также, это необходимо для написания Unit-тестов.

Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons

Автор: chernikov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js