Використання єдиного IoC Container'a в рамках HTTP-запиту між Web API і OWIN Middleware

Метою даної статті є пошук робочого рішення, яке дозволяє мати єдиний контейнер залежностей (IoC контейнер) протягом усього життєвого циклу запиту, контролювати його створення і знищення.

image

Це може знадобитися в тому випадку, якщо web-додаток повинен мати транзакционность (а на мій погляд будь-web-додаток його зобов'язане мати, тобто застосовувати зміни (наприклад, в БД) тільки у разі успішної обробки запиту і робити їх скасування, якщо на будь-якому з етапів виникла помилка, що свідчить про некоректну результаті і неконтрольованих наслідки) (github source code).

Теорія
Проекти Web API 2 конфігуруються за допомогою OWIN інтерфейсу IAppBuilder, який покликаний допомогти побудувати pipeline обробки вхідного запиту.

На зображенні вище видно життєвий цикл запиту,- він проходить по всім компонентам ланцюжка, потім потрапляє в Web API (що також є окремим компонентом) і повертається назад, формуючи або декоруючи відповідь від сервера.

Для того, щоб мати єдиний контейнер залежностей нам потрібно створювати його явно на початку обробки запиту і знищувати по завершенню:

  1. Початок обробки запиту;
  2. Створення контейнера;
  3. Використання контейнера в Middleware;
  4. Використання контейнера Web API;
  5. Знищення контейнера;
  6. Завершення обробки запиту.
Для цього нам достатньо налаштувати контейнер, зареєструвати його в Web API (за допомогою DependencyResolver):

// Configure our parent container
var container = UnityConfig.GetConfiguredContainer();

// Pass our parent container to HttpConfiguration (Web API)
var config = new HttpConfiguration {
DependencyResolver = new UnityDependencyResolver(container)
};

WebApiConfig.Register(config);

Написати власний Middleware, який буде створювати дочірній контейнер:

public class UnityContainerPerRequestMiddleware : OwinMiddleware
{
public UnityContainerPerRequestMiddleware(OwinMiddleware next, IUnityContainer container) 
: base(next)
{
_next = next;
_container = container;
}

public override async Task Invoke(IOwinContext context)
{
// Create child container (whose parent is global container)
var childContainer = _container.CreateChildContainer();

// Set created container to owinContext 
// (to become available at other places using OwinContext.Get<IUnityContainer>(key))
context.Set(HttpApplicationKey.OwinPerRequestUnityContainerKey, childContainer);

await _next.Invoke(context);

// Dispose container that would dispose each of container's registered service
childContainer.Dispose();
}

private readonly OwinMiddleware _next;
private readonly IUnityContainer _container;
}

І використовувати його в інших Middleware'ах (в моїй реалізації я зберігаю контейнер в глобальному OwinContext з допомогою context.Set, який передається в кожен наступний middleware і отримую його з допомогою context.Get):

public class CustomMiddleware : OwinMiddleware
{
public CustomMiddleware(OwinMiddleware next) : base(next)
{
_next = next;
}

public override async Task Invoke(IOwinContext context)
{
// Get container that we set to OwinContext using common key
var container = context.Get<IUnityContainer>(
HttpApplicationKey.OwinPerRequestUnityContainerKey);

// Resolve registered services
var sameInARequest = container.Resolve<SameInARequest>();

await _next.Invoke(context);
}

private readonly OwinMiddleware _next;
}

На цьому можна було б закінчити, якби не одне .

Проблема
Middleware Web API всередині себе має свій власний цикл обробки запиту, який виглядає наступним чином:

  1. Запит потрапляє в HttpServer для початку обробки HttpRequestMessage і передачі його в систему маршрутизації;
  2. HttpRoutingDispatcher витягує дані з запиту, і з допомогою таблиці Route'ов визначає контролер, відповідальний за обробку;
  3. HttpControllerDispatcher створюється певний раніше контролер і викликається метод обробки запиту з метою формування HttpResponseMessage.
За створення контролера відповідає наступна рядок DefaultHttpControllerActivator:

IHttpController instance = (IHttpController)request.GetDependencyScope().GetService(controllerType);

Основний вміст методу GetDependencyScope:

public static IDependencyScope GetDependencyScope(this HttpRequestMessage request) {
// ...

IDependencyResolver dependencyResolver = request.GetConfiguration().DependencyResolver;
result = dependencyResolver.BeginScope();

request.Properties[HttpPropertyKeys.DependencyScope] = result;
request.RegisterForDispose(result); 

return result;
}

З нього видно, що Web API запитує DependencyResolver, який ми для нього зареєстрували в HttpConfiguration і з допомогою dependencyResolver.BeginScope() створює дочірній контейнер, в рамках якого і буде створений екземпляр відповідального за обробку запиту контролера.

Для нас це означає наступне: контейнер, який ми використовуємо в наших Middleware'ах і в Web API не є одними й тими ж,- більше того, вони знаходяться на одному рівні вкладеності, де глобальний контейнер — їх загальний батько, тобто:

  1. Глобальний контейнер;
    1. Дочірній контейнер, створений в UnityContainerPerRequestMiddleware;
    2. Дочірній контейнер, створений в Web API.

Для Web API це виглядає цілком логічним у тому випадку, коли воно є єдиним місцем обробки запиту — контейнер створюється спочатку і знищується в кінці (це рівно те, чого ми намагаємося досягти).

Однак, в даний момент Web API є лише однією з ланок в pipeline, а значить від створення власного контейнера доведеться відмовитися,- нашим завданням є перевизначити даний поведінку і вказати контейнер, в рамках якого Web API потрібно створювати контролери та Resolve'ить залежності.

Рішення
Для вирішення вище поставленої проблеми ми можемо реалізувати власний IHttpControllerActivator, в методі Create якого будемо отримувати створений раніше контейнер і вже в рамках нього Resolve'ить залежності:

public class ControllerActivator : IHttpControllerActivator
{
public IHttpController Create(
HttpRequestMessage request,
HttpControllerDescriptor controllerDescriptor,
Type controllerType
)
{
// Get container that we set to OwinContext using common key
var container = request.GetOwinContext().Get<IUnityContainer>(
HttpApplicationKey.OwinPerRequestUnityContainerKey);

// Resolve requested IHttpController using current container
// prevent DefaultControllerActivator's behaviour of creating child containers 
var controller = (IHttpController)container.Resolve(controllerType);

// Dispose container that would dispose each of container's registered service
// Two ways of disposing container:
// 1. At UnityContainerPerRequestMiddleware, after owin pipeline finished (WebAPI is just a part of pipeline)
// 2. Here, after web api pipeline finished (if you do not use container at other middlewares) (uncomment next line)
// request.RegisterForDispose(new Release(() => container.Dispose()));

return controller;
}
}

Для того, щоб використовувати його в Web API все що нам залишається, це замінити стандартний HttpControllerActivator в конфігурації:

var config = new HttpConfiguration {
DependencyResolver = new UnityDependencyResolver(container)
};

// Use our own IHttpControllerActivator implementation 
// (to prevent DefaultControllerActivator's behaviour of creating child containers per request)
config.Services.Replace(typeof(IHttpControllerActivator), new ControllerActivator());

WebApiConfig.Register(config);

Таким чином, ми отримуємо наступний механізм роботи з нашим єдиним контейнером:

1. Початок обробки запиту;

2. Створення дочірнього контейнера від глобального;

var childContainer = _container.CreateChildContainer();

3. Присвоювання контейнера в OwinContext:

context.Set(HttpApplicationKey.OwinPerRequestUnityContainerKey, childContainer);

4. Використання контейнера в інших Middleware'ах;

var container = context.Get<IUnityContainer>(HttpApplicationKey.OwinPerRequestUnityContainerKey);

5. Використання контейнера Web API;

5.1. Отримання контролера з OwinContext:

var container = request.GetOwinContext().Get<IUnityContainer>(HttpApplicationKey.OwinPerRequestUnityContainerKey);

5.2. Створення контролера на основі цього контейнера:

var controller = (IHttpController)container.Resolve(controllerType);

6. Знищення контейнера:

childContainer.Dispose();

7. Завершення обробки запиту.

Результат
Конфігуруємо залежності у відповідності з необхідними нам їх життєвими циклами:

public static void RegisterTypes(IUnityContainer container)
{
// ContainerControlledLifetimeManager - singleton's lifetime
container.RegisterType<IAlwaysTheSame, AlwaysTheSame>(new ContainerControlledLifetimeManager());
container.RegisterType<IAlwaysTheSame, AlwaysTheSame>(new ContainerControlledLifetimeManager());

// HierarchicalLifetimeManager - container's lifetime
container.RegisterType<ISameInARequest, SameInARequest>(new HierarchicalLifetimeManager());

// TransientLifetimeManager (RegisterType's default) - no lifetime
container.RegisterType<IAlwaysDifferent, AlwaysDifferent>(new TransientLifetimeManager());
}

  1. ContainerControlledLifetimeManager — створення єдиного екземпляра в рамках програми;
  2. HierarchicalLifetimeManager — створення єдиного екземпляра в рамках контейнера (де ми домоглися того, що контейнер єдиний в рамках HTTP запиту);
  3. TransientLifetimeManager — створення екземпляра при кожному зверненні (Resolve).
image
У зображенні вище відображені GetHashCode'и залежностей в розрізі декількох HTTP запитів, де:

  1. AlwaysTheSame — singleton об'єкт в рамках програми;
  2. SameInARequest — singleton об'єкт в рамках запиту;
  3. AlwaysDifferent — новий екземпляр для кожного Resolve.
» Вихідні матеріали доступні на github.

Матеріали:
1. Конвеєр в ASP.NET Web API
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.