понедельник, 14 декабря 2015 г.

Time scope в Ninject

Постановка задачи

Есть некий data-provider, собирающий информацию из разных источников (локальная сеть, файловая система, ресурсы интернета) и предоставляющий программе некоторую сводную информацию. При этом, сбор и подготовка информации занимает 1-5 секунд, а обращение к провайдеру происходит несколько раз в секунду. Однако, исходная информация обновляется достаточно медленно и небольшое устаревание (в пределах нескольких часов) вполне допустимо. Очевидное решение задачи - это кэш. Кэш - это хорошо, это правильно, но хотелось бы так, что бы попроще - 1 строчка кода.. хм, ну максимум две..

Ninject решение

И тут я вспомнил, что мой провайдер лежит в биндингах Ninject-а. Эврика! Достаточно добавить к биндингу "волшебную"строчку .InSingletonScope() и будет "счастье". При запуске программы собираются данные и оседают в синглтон-объекте до конца жизни программы. В момент следующего запуска, данные снова собираются свежие. И, все бы хорошо, если бы не один нюанс - провайдер захотелось поместить в службу на сервере, которая не перезапускается по пол года. Ок. Хочется тот же синглтон, но так, что бы он все-таки пересоздавался.. иногда, ну, например, раз в два часа. Для реализации такого функционала в Ninject есть понятие custom scope - метод .InScope(...). В качестве параметра данный метод принимает функцию, которая, в свою очередь, должна вернуть произвольный объект. Именно на этот объект и будет ориентироваться система - если он изменится (будет отличаться от того, который функция вернула в прошлый раз), то наш провайдер будет пересоздан. Осталось понять, где взять такой объект, который будет пересоздаваться через нужный временной интервал. Создавать громоздкую систему со статическими hash-таблицами совершенно не хотелось (тогда уж лучше сделать кэш). И тут пришла в голову идея - что если в качестве такого объекта использовать специальное число, полученное следующим способом:
var diff = DateTime.Now.Ticks / time.Ticks;
Где time - это нужный интервал времени (TimeSpan). И, все бы хорошо, но! Ninject, как выяснилось путем простого чтения исходных кодов (и как это я сразу не догадался туда заглянуть), сравнивает объект по ссылке, т. е. с помощью метода object.ReferenceEquals(...), а не "по-человечески" с помощью object.Equals(...), как я наивно ожидал. В итоге, простые типы в качестве специального объекта не работают.

"Магия" строк

Вот тут-то я и решил использовать "магию". Итак, мне нужен объект, который будет правильно сравниваться по ReferenceEquals и, при этом, будет вычисляться "на лету", что бы не хранить его ни в какой статике и не думать о накапливаемом "мусоре". Хм. Вообще-то такое сделать нельзя - без статического хранилища эта задача не решается. Но! можно использовать строки. Дело в том, что внутри .Net в строках уже заложено статическое хранилище и оно поддерживается системой. А, значит, нам достаточно этим воспользоваться. Добавим еще одну строчку кода и обернем это в удобную функцию, которую можно будет использовать многократно:
object GetTimeScope( TimeSpan time )
{
   var diff = DateTime.Now.Ticks / time.Ticks;
   return string.Intern( diff.ToString() );
}
Главная "магия" тут в слове Intern - именно оно и даст нам правильную работу ReferenceEquals. Ну а использовать этот метод совсем просто:
kernel.Bind<TestDataObject>().ToSelf().InScope( c => GetTimeScope( TimeSpan.FromHours( 2 ) ) );
Удачного программирования.