Скрытый построитель в процессе работыё


Думаю, никому здесь не требуется объяснять, что такое кэширование и чем оно полезно, указывать, что кэширование широко используется, и упоминать, что для его реализации во многих средах программирования есть стандартные компоненты. Поэтому приступлю сразу к делу.


В .NET кэширование в оперативной памяти реализует пакет Microsoft.Extensions.Caching.Memory, входящий в набор .NET Extensions. И поводом для написания этой статьи послужили приключения (с успешным концом), связанные с упомянутым в заголовке интерфейсом ICacheEntry из этого пакета, возникшие при попытке его нестандартного использования.


Но рассказать я хочу не только о той недокументированной засаде, в которую я попал, сделав шаг в сторону от примеров использования из документации. И не только о том, как я из нее выбрался. Дело в том, что при выяснении правильного способа работы с ICacheEntry я наткнулся на довольно необычный приём программирования (он же Design Pattern), который я для себя назвал "Скрытый построитель". И наткнулся я на него в коде библиотек .NET не в первый раз. И я раньше нигде не читал про подобный приём. А потому я решил включить в статью ещё и описание этого приёма. А так как этот приём не специфичен для C#, и его вполне можно использовать и на других языках, то он может быть интересен и тем, кто не работает с C# и .NET.


Введение


Для той задачи, которую мне понадобилось решить — хранения в кэше произвольных объектов — в пакете Microsoft.Extensions.Caching.Memory есть интерфейс IMemoryCache и доступный публично реализующий его класс MemoryCache. В пакете также есть довольно много методов расширения для интерфейса IMemoryCache, которые позволяют добавлять в кэш объекты с разными связанными с ними свойствами, такими, например, как время жизни объекта в кэше. Использование этих методов неплохо документировано, эти методы позволяют решать многие задачи, связанные с кэшированием. Многие — но не все. Кроме того, для использования некоторых из этих методов требуется код, кажущийся излишне громоздким.


Для решения нестандартных задач и уменьшения громозкости у меня возникла мысль использовать методы интерфейса IMemoryCache напрямую. И интерфейс это, вроде бы, позволяет. В этом интерфейсе определен метод CreateEntry, который возвращает интерфейс ICacheEntry к вновь созданному элементу кэша, через который возможен доступ к ключу, значению и прочим свойствам создаваемого элемента. Таким образом, кажется возможным создать элемент кэша и установить его свойства вручную. Но при попытке так сделать меня поджидала, буквально, засада. Причём, в документации о ней нет ни слова.


История


Все началось с того, что при написании некой библиотеки, которой я занимаюсь на досуге, возникла необходимость сохранять в кэше определенные объекты. Для чего был написан такой код (примерно такой: для краткости обработка исключений здесь убрана; на правую часть — справа от знака присваивания, с неизвестными именами переменных и с вызовами конструкторов неизвестных типов — внимание можно не обращать: для темы статьи интересна только левая часть, а правая — только для иллюстрации):


   ICacheEntry new_entry = _memoryCache.CreateEntry(key);
   new_entry.SlidingExpiration=_idleTimeout;
   new_entry.AbsoluteExpirationRelativeToNow=_maxLifetime;
   new_entry.Size=1; 
   new_entry.Value=result=new ActiveSession(_rootServiceProvider.CreateScope(), this, Session, _logger, trace_identifier);
   PostEvictionCallbackRegistration end_activesession = new PostEvictionCallbackRegistration();
   end_activesession.EvictionCallback=EndActiveSessionCallback;
   end_activesession.State=trace_identifier;
   new_entry.PostEvictionCallbacks.Add(end_activesession);

В документации подробности работы метода IMemoryCache.CreateEntry не описаны, поэтому код выше основывался на предположении. Предположение состояло в том, что этот метод создает элемент в кэше и возвращает интерфейс ICacheEntry, позволяющий установить затем в нем нужные свойства. Это предположение казалось логичным, даже чуть ли не единственно верным, потому что специального метода сохранения установленных свойств (в том числе — кэшируемого значения) для элемента кэша в интерфейсе ICacheEntry нет. Однако этот код не работает: после вызова CreateEntry счётчик элементов в кэше и не думает увеличиваться, объект в кэше не сохраняется и, соответственно, потом не находится. А так как документация совсем не описывает самостоятельное использование ICaheEntry, пришлось лезть в исходный код пакета.


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


public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value)
{
    using ICacheEntry entry = cache.CreateEntry(key);
    entry.Value = value;

    return value;
}

Странное — это оператор using: его наличие в начале метода позволяет добавить в конце этого метода в кэш полученный элемент, на который ссылается ICacheEntry. Чудеса? Не совсем. Но чтобы понять, как это работает, нужно вспомнить, что собой представляет оператор using в C#.
Думаю, все, сколь-нибудь активно использующие C#, знакомы с идиомой очищаемых (disposable) объектов и используемым в ней методом Dispose() интерфейса IDisposable (от этого интерфейса, кстати, унаследован интерфейс ICacheEntry, на что я сразу и не обратил внимания).


Вкратце, однако, напомню...

… зачем и почему он используется. Идея тут в том, что если объект содержит ссылки на неуправляемые ресурсы, которые сборщик мусора самостоятельно, без финализатора, освободить не может — например, описатели (handle) ресурсов операционной системы — или эксклюзивно используемые дочерние объекты, содержащие такие ссылки, то в таких объектах принято реализовывать интерфейс IDisposable. Для скорейшего освобождения таких ресурсов после того, как использование закончено, следует вызвать метод Dispose(), в задачу которого входит освобождение неуправляемых ресурсов и/или вызов методов Dispose() дочерних объектов. Конечно, можно не заморачиваться очищением всего дерева таких объектов через Dispose(), а положиться на сборщик мусора и финализаторы для объектов, непосредственно владеющих неуправляемыми ресурсами, освобождающие эти ресурсы: такие объекты в любом случае обязаны реализовать финализаторы. Но это заметно замедляет работу сборщика мусора. Так что идиома очищаемых объектов — это вещь полезная.


Эта идиома в C# используется настолько часто, что уже давно разработчики языка добавили в него оператор using, который скрывает детали её использования от глаз пользователя.


Подробности

конструкция


using (var variable=expression/*выражение_создающее_объект*/) { 
    /*действия_с_созданным_объектом*/ 
} 

эквивалентна последовательности операторов


var variable=expression/*выражение_создающее_объект*/;
try {
  /*действия_с_созданным_объектом*/ 
}
finally {
  variable.Dispose();
}

То есть, оператор using не только избавляет программиста от необходимости знать про идиому очистки объектов и интерфейс IDisposable, но и экономит немалое количество нажатий на клавиши. Тем самым — увеличивает ключевой ( ;-) ) показатель нынешнего процесса разработки — скорость написания кода. Ну, а в дальнейшем разработчики языка C# пошли ещё дальше по пути сокращения числа нажатий на клавиши и сэкономили ещё пару: сделали фигурные скобки вокруг тела оператора using необязательными — в нынешнем C# считается, что оператор using без фигурных скобок действует до конца включающего его блока. И пусть код выглядит для непривычного человека загадочным, но повышение скорости написания кода достигнуто.


Короче, загадочный using в начале блока с телом метода — это вызов метода Dispose() в конце этого блока для переменной entry из using. Ловко придумано, запутывает замечательно ;-). Осталось только разобраться, как этот вызов Dispose(), предназначенный, вообще-то, для очистки более не используемого объекта, помогает решать задачу сохранения элемента в кэше.


Начать расследование стоит с метода создания элемента кэша MemoryCache.CreateEntry: как оказалось, этот метод создает независимый экземпляр класса CacheEntry — внутреннего класса, реализующего публичный интерфейс ICacheEntry — и возвращает этот интерфейс. Слово "независимый" выше означает, что вновь созданный элемент в список элементов кэша на этой стадии пока что не добавлется.


Подробности создания

В конструктор CacheEntry передается ключ элемента и ссылка на объект кэша. Именно она будет использована впоследствии для добавления этого элемента в кэш, о чём — ниже. Кроме того, вновь созданный элемент кэша может быть добавлен (а в версиях .NET до 6.0 включительно — обязательно добавляется) в список связанных записей кэша (подробнее об этом механизме — в следующем спойлере).


А просмотр исходного кода метода CacheEntry.Dispose() показывает, что этот метод действительно содержит вызов внутреннего метода MemoryCahe.SetEntry(). Метод же SetEntry(), в полном соответствии со своим названием, добавляет в кэш этот независимый элемент CacheEntry. А если элемент с таким же ключом в кэше уже был, то этот старый элемент удаляется (evict) из кэша с кодом причины удаления EvictionReason.Replaced.


А также Dispose() делает ещё кое-что нетривиальное: обрабатывает связанные элементы

В логике создания независимых экземпляров CacheEntry и сохранения их в кэше есть дополнительная возможность — создание связанных элементов. Я не видел, чтобы она где-то была документирована для использования в приложениях (раздел "Cache Dependencies" в документации по "In-memory caching" — это не о том), и чтобы к ней давал доступ какой-то из методов расширения. Однако, судя по тому, что создатели .NET уделили много времени оптимизации этого механизма (это видно по коду и комментариям в нем), им пользуется какой-то внутренний код библиотеки .NET, и, причем — достаточно часто используемый. Но, в принципе, возможность эта вполне доступна снаружи.


Смысл этой возможности в том, что можно создать несколько независимых элементов, не записывая их в кэш, и установить ряд свойств только для последнего элемента — и эти свойства будут скопированы на все такие элементы при поочередном сохранении элементов в кэше (уже упомянутым методом Dispose()). В число этих свойств входят абсолютное время устаревания элемента и список маркеров изменения (реализаций интерфейса IChangeToken), срабатывание которых приводит к удалению элемента из кэша. При этом, метод Dispose() для нескольких элементов кэша должен вызываться обязательно в порядке, обратном порядку их создания вызовом MemoryCache.CreateEntry, иначе механизм работать не будет (в режиме отладки правильность порядка проверяется с помощью Debug.Assert).
Реализована связь независимых элементов CacheEntry в виде стека: голова стека — текущий создаваемый элемент — записывается в конструкторе в переменную _current типа AsyncLocal<CacheEntry> (т.е., своего рода "статическую" в рамках потока асинхронного выполнения: она привязана к контексту выполнения, а потому процесс создания связанных элементов можно выполнять и в async-методе: продолжения после await увидят то же самое значение). А каждый элемент CacheEntry содержит поле CacheEntry _previous, ссылающееся на предыдущий созданный независимый элемент: в него в конструкторе копируется значение _current.Value, при этом первый созданный элемент (а также — все элементы, уже сохраненные в кэше) будет содержать в нем null. Именно поэтому методы Dispose() нескольких независимых элементов надо вызывать в порядке, обратном порядку их создания.
В .NET Core/.NET до версии 7.0 этот механизм был включен всегда. Но, начиная с .NET 7.0, разработчики сделали этот механизм отключенным по умолчанию из соображений производительности, а для его включения добавили свойство MemoryCacheOptions.TrackLinkedCacheEntries. Так что, если захотите пользоваться — имейте это в виду.


И действительно, добавление в конец написанного выше кода вызова метода Dispose() успешно сохраняет нужный элемент в кэше. Итак, загадка решена.


Вроде бы, загадка решена, но...

Решение наводит на некие (хорошие или нет — не знаю) мысли, которые лучше всего выражаются словами "а что это за...?". Программисты, работающие с .NET, привыкли, что IDisposable.Dispose() — это очистка, освобождение не управляемых CLR ресурсов, которые GC(сборщик мусора) сам освободить не в состоянии (точнее — не в состоянии освободить без помощи метода завершения (finalizer), который весьма заметно нагружает GC лишней работой).


Но здесь-то никаких неуправляемых ресурсов нет — ведь MemoryCache использует только управляемую память. Вместо этого на ICacheEntry.Dispose() была навешена функциональность, никак не предусмотренная в его предке — IDisposable. Конечно, похожее поведение встречается и с другими объектами: при работе с файлами, запросами к БД и т.п. метод Dispose() производит завершающие операции ввода-вывода. Но там это — часть очистки, а тут — совершенно отдельная функциональность. А это как-то не очень бьется с рекомендациями теоретиков.


Как к этому относиться? С одной стороны, чистота кода — соответствие неким теоретическим принципам — меня особо не волнует: со своим многолетним опытом я привык относиться к ним философски. Ну, плачет там где-то буква L в акрониме SOLID от того, что не соблюдается принцип подстановки, который она олицетворяет — а мне-то что с того? Но, с другой стороны, конкретно с ICacheEntry все-таки была неожиданная засада, и я потратил несколько часов, чтобы ее обойти. Я понимаю, что в проекте .NET Core кому-то когда-то захотелось пооригинальничать, а теперь это уже не исправишь (это было признано более 5 лет назад в давнем, закрытом issue в GitHub). Но можно было бы хотя бы такое поведение описать в документации: мне бы, например, это помогло, так как я имею привычку ее читать заранее. Но нет, последний issue с просьбой об этом (возможно, есть и более раннние, не искал) в GitHub с предложением документировать такое неожиданное поведение ICacheEntry висит уже больше года без движения. Вообще-то, и в мире Open Source в целом с документацией не слишком хорошо. Например, общедоступная документация по широко применяемому и имеющему довольно много разных возможностей фреймворку имитации объектов Moq — это одна страничка на GiHub с примерами применения, и не более того. И только после довольно длительных поисков я разыскал хотя бы документацию по классам и методам Moq (похоже, сделанную на основе XML-документации непосредственно в коде). Но для Open Source такое простительно: разработчики Open Source обычно не имеют ни достаточных ресурсов, ни достаточной мотивации, чтобы делать хорошую документацию, а кому очень надо — вот код, пусть ищут ответы там. Но .NET, хоть его исходный код и открыт — это все-таки разработка Microsoft, крупной корпорации, у которой явно есть ресурсы и возможности замотивировать своих работнков. Но нет, чувствуется в этом некое снисходительное отношение Microsoft к разработчикам, как к людям ограниченым: пусть используют продукт только так, как предусмотрено, а шаг влево, шаг вправо — не нужен, ибо нефиг. Но, собственно, Microsoft всегда была такой, самодеятельность и левшизм с кулибинщиной она никогда не поощряла, так что к этому можно было бы и привывкнуть(я привык, если что).


Но это ещё не всё, о чём я хотел рассказать.


Шаблон проектирования "Скрытый построитель"


Я уже несколько раз встречал в системных библитеках .NET код, в котором инициализация объекта после его конфигурирования происходит внезапно, в неочевидном месте и неочевидным способом. Например, точно так же в неожиданном месте — при первом обращении к их списку — инициализуются и экземпляры объекта конечной точки (тип которого — класс-наследник Endpoint) в подсистеме маршрутизации ASP.NET Core.


Немного подробностей про конфигурирование и инициализацию Endpoint

Для добавления конечной точки подсистема маршрутизации ASP.NET Core предоставляет интерфейс IEndpointRouteBuilder. Для приложений на базе WebHost/GenericHost этот интерфейс передается как единственный параметр в делегат, передаваемый как аргумент в метод расширения UseEndpoints(парный методу UseRouting) интерфейса IApplicationBuilder. А интерфейс IApplicationBuilder передается как параметр в метод Configure startup-класса. Для приложений на появившемся в .NET 6.0 шаблоне веб-приложения, базирующемся на WebApplication, где методы расширения UseRouting/UseEndpoints явно использовать не требуется (нужные для работы маршрутизации компоненты-обработчики, добавляемые этими методами, добавляются в конвейер объектов-обработчиков неявно), а интерфейс IEndpointRouteBuilder реализован самим классом WebApplication.
Конечные точки маршрутизации добавляются через методы расширения интерфейса IEndpointRouteBuilder. Существует довольно много таких методов, как в базовом фреймворке, так и в его расширениях: MVC, Razor Pages, Blazor и т.д. Такой метод расширения интерфейса IEndpointRouteBuilder добавляет в соответствующий им источник данных маршрутизации специфичный для источника данных внутренний класс-построитель конечной точки. Этот класс обычно реализует интерфейс IEndpointConventionBuilder. Именно этот интерфейс IEndpointConventionBuilder возвращается методом расширения и используется для конфигурирования свойств конечной точки. Кроме того класс-построитель использует класс-наследник абстрактного класса EndpointBuilder для создания конечной точки нужного типа. Публично доступен только один класс-наследник EndpointBuilder — RouteEndpointBuilder, создающий экземпляр конеченой точки RouteEndpoint. Создание же всех объектов конечной точки происходит, как и упоминалось выше, при первом обращении к списку конечных точек в источнике данных уже внутри класса EndpointRoutingMiddleware (этот класс создается и устанавливается в конвейер компонентов-обработчиков, чтобы находить конечную току маршрутизации для запроса). Но это — в норме, потому что список конечных точек, в принципе, доступен приложению пользователя через IEndpointRouteBuilder, и плохо сконструированное приложение может преждевременно обратиться к списку конечных точек в источнике данных маршрутизации, вызвав преждевременное построение объектов конечных точек.
PS А в целом подсистема маршрутизации ASP.NET Core и как она работает внутри себя — это очень интересная и довольно большая тема. В отдаленных планах у меня есть намерения написать одну или несколько статей об этом, но пока что это — только планы.


И я увидел в таких примерах ранее не встречавшийся мне прием программирования, он же — шаблон проектирования (Design pattern), который я назвал для себя "Скрытый построитель". Он похож на общеизвестный шаблон "Построитель" (Builder), предназначенный для создания сложных объектов, имеющих сложный набор компонентов-свойств, которые могут быть установлены по-разному. В рамках шаблона "Построитель" для этого создается специальный объект построителя, через который конфигурируются компоненты создаваемого сложного объекта, после чего вызывается метод, создающий этот сложный объект. Такой подход достаточно широко используется в .NET: например, в ASP.NET веб-приложение во всех вариантах его построения (Web Host, Generic Host, WebApplication) строится именно по этому шаблону: создаётся объект построителя (соответственно, WebHostBuilder/HostBuilder/WebApplicationBuilder), производится настройка этого объекта и, наконец, создаётся объект приложения вызовом метода Build построителя.


Так вот, в шаблоне "Скрытый построитель" построение сложного объекта начинается примерно так же: создаётся объект построителя (в нашем случае — независимый CacheEntry) и производится его конфигурирование (в нашем случае — через реализуемый им интерфейс ICacheEntry). А вот само создание объекта производится не явным вызовом специального метода, а некой операцией, от которого такое действие совершенно не ожидается (в нашем случае создание элемента в кэше выполняется, как описано выше, методом Dispose, предназначенным, по идее, совсем для другого).


Где можно использовать приём "Скрытый построитель"? Если осознанно — то, разве что, для написания "чистого непонятного кода": кода, формально соответствующего критериям "чистого кода" от теоретиков, но, при этом, все равно ускользающего от понимания. Такая необходимость возникает, например, при обеспечении job security. Или — для защиты интеллектуальной собственности в открытом коде: применение нетривиального приёма программирования безо всяких намеков на него в документации вполне может быть эффективно использовано для того, чтобы отбить охоту лезть в этот код у желающих странного и пытающихся не ограничиваться небольшим набором рекомендуемых приемов использования, документированных в примерах. Но, вообще-то, код, соответствующий приёму "Скрытый построитель", может быть и дикорастущим — получаться сам по себе в процессе написания: например, потому что так короче. Как обстоит дело в данном случае, я точно не знаю: я не готов тут настаивать на обвинении Microsoft в недобрых намерениях, но я не первый раз наблюдаю подобные сомнительные действия у Microsoft, так что от подозрений избавиться не могу.


Является ли шаблон "Скрытый построитель" "антипаттерном"? Об этом пусть спорят теоретики, меня же такой вопрос не волнует. Но лично для себя я решил избегать использовать этот приём — кроме как в случаях, когда действительно требуется написать именно непонятный код. Но я надеюсь, что мне это делать не придется.


Заключение


В статье рассмотрен правильный (и нетривиальный) способ непосредственного, т.е., без помощи методов расширения, использования итерфейса ICacheEntry для настройки и добавления элементов в кэш.


В процессе поиска этого способа был обнаружен ранее неизвестный мне приём программирования (иначе шаблон проектирования) "Скрытый построитель" (рискну преревести его как "Disguised Builder" design pattern). Рассмотрены аргументы за и против его использования.


PS. Если бы у меня был свой телеграмм-канал, то здесь, как модно у современных авторов Хабра, я оставил бы ссылку на него. Но у меня нет своего телеграмм-канала.
А картинка к посту — это как видит скрытый построитель в процессе работы нейросеть "Кандинский".

Комментарии (18)


  1. panzerfaust
    04.09.2023 04:06
    +2

    Где можно использовать приём "Скрытый построитель"? Если осознанно — то, разве что, для написания "чистого непонятного кода": кода, формально соответствующего критериям "чистого кода" от теоретиков

    Ну вообще-то любой "теоретик" чистого кода скажет, что метод Dispose должен именно что диспозить и ничего более. Иначе SRP и LSP идут лесом. А следовательно, здесь имеем некий буллшит, а не попытку в "чистый код". А сам кейс - еще одно подтверждение того, что любые авторитеты (кодеры MS в данном случае) лажают.


    1. saege5b
      04.09.2023 04:06
      -2

      МС всегда использовала схему "мутного" и "не документированного" кода. Со времён 3.11, когда всякие "обвесы" и "тайные практики программирования" прямо косяками ходили.

      И за МС наблюдается практика, когда она подобные велограблевилопеды немного "модернизировала" от обновления к обновлению, что порой приводило к "забавным"/"эффектным" последствиям.

      Просто бизнес, ничего личного (с).


      1. ksbes
        04.09.2023 04:06
        +2

        Я тут исхожу из бритвы Хеллона - скорее всего просто так было удобно написать данный конкретный компонент данному конкретному разработчику. Ну, например, чтобы гарантировать отсутствие утечек памяти при добавлении объектов в кэш (добавить без диспоуза невозможно, если диспоуз и осуществляет добавление! - гениально!)

        По поводу паттерна "скрытый строитель" - он нужен как и всё скрытое, когда мы не хотим мучить пользователя деталями своей реализации, а хотим сразу дать ему "кнопочку" "ехать". Потому втихую посоздавать/поуничтожать объекты в удобный для нас момент жизненного цикла (управляемого пользователем библиотеки) - это нормально. А нестандарт жизненного цикла мы пользователю и не обещали!


        1. saege5b
          04.09.2023 04:06
          -2

          Ещё во времена Windows 95 было замечено, что если писать по документации от МС, то часто получается медленно и коряво, при том, что аналог от самих МС работает быстро и гладко.


        1. ritorichesky_echpochmak
          04.09.2023 04:06
          +1

          Dispose сам по себе, при таком неочевидном кейсе не гарантирует ну никак отсутствие утечек памяти. Получается скорее в обратную сторону - в тот момент, когда ты ожидаешь что объект диспоузнулся (и в следующий раз по логике ты должен получить ObjectDisposedException!) объект добавляется куда-то "на долгую память" и становится ещё менее очевидно, как правильно его оттуда выковыривать не вызывая сборку мусора по всем кругам ада.
          Почему этот явно антипаттерн не переписали более очевидным способом - ну очень хотелось бы знать


          1. slonopotamus
            04.09.2023 04:06

            А еще становится непонятно как НЕ добавить энтри в случае ошибки.


            1. mvv-rus Автор
              04.09.2023 04:06

              НЕ добавить — это как раз просто: НЕ вызывать Dispose() в случае ошибки при конфигурировании. Для этого даже не надо делать специальную обработку исключений, можно просто писать код подряд, сначала — установку свойств, в конце — Dispose()


              1. mayorovp
                04.09.2023 04:06

                Но это противоречит общепринятым практикам использования Dispose — через using…


                А, и ещё это приведёт к утечке памяти если включено то странное отслеживание.


                1. mvv-rus Автор
                  04.09.2023 04:06

                  Но это противоречит общепринятым практикам использования Dispose — через using…

                  О том и статья, собственно. Да, это нехорошо и контринтуитивно, но кто-то из разработчиков .NET Core когда-то решил сделать так — и это уже не исправишь, не нарушив совместимость. Только вот почему это ещё и не документировано? Разумного объяснения (ну, кроме очевидного — лень/нетбабла) для этого я не нахожу.
                  А, и ещё это приведёт к утечке памяти если включено то странное отслеживание.

                  Вряд ли: тот код всего лишь переносит установленные свойства с добавляемого в элемента на следующий, который нужно будет добавить, не более того. Причем, ни Key, ни Value в число переносимых свойств не входят. Так что на отслеживание памяти это само по себе влиять не должно.


                  1. mayorovp
                    04.09.2023 04:06

                    Проблема не во влиянии на отслеживание памяти, а в тот, что все забытые ICacheEntry выстраиваются в односвязные списки начиная с ссылки в AsyncLocal-переменной.


                    1. mvv-rus Автор
                      04.09.2023 04:06

                      Да, они будут жить столько, сколько будет жить соответствующий ExecutionContext. Но, насколько я понимаю, в ASP.NET Core это будет недолго: там каждый запрос связан со своим ExecutionContext (например, именно через него работает HttpContextAccessor — реализция по умолчанию для службы IHttpContextaccessor, служащей ныне в качестве средства доступа к текущему HttpContext заменой для HttpContext.Current времен ASP.NET Framework).
                      То есть, AsyncLocal-ссылки в норме будут жить только в течение времени обрабоки запроса.
                      Так?
                      То есть, если моя библиотека предназначена для ASP.NET Core даже версии 6.0, то я могу смело забывать ссылку на независимый CacheEntry — долго он не проживует, т.к. долго не проживет и ExecutionContext, который тоже имеет ссылку него.


                      1. mayorovp
                        04.09.2023 04:06

                        Проблема в том, что существует такая штука как воркеры. И мидлвари Kestrel (не путать с мидлварями ASP.NET Core).


                      1. mvv-rus Автор
                        04.09.2023 04:06

                        По этому поводу можно было бы, покопавшись в потрохах, поспорить и ещё, конечно.
                        Но, в общем и в целом, вы меня убедили: негоже оставлять ссылку на свой объект в другом объекте (ExecutionContext), время жизни которого я не контролирую. Надежнее будет делать Dispose() всегда, а в случае ошибки — вызывать Remove.
                        Пошел править код библиотеки.


                      1. mikegordan
                        04.09.2023 04:06
                        -1

                        Почти так, но вы пропустили этапы .net core до 3.0 , но не суть.

                        Контекст как вы описали жил еще во времена IIS когда , контекст был жестко привязан в flow цепочки обработки. Сейчас контекст живет как обычный объект в разрезе жизни потока и сборщика мусора.

                        Пример с замыканиями описал тут (ниже) , когда контекст живет после отдачи клиенту ответа и закрытия соединения:

                        https://habr.com/ru/articles/755778/#comment_25933458


            1. mvv-rus Автор
              04.09.2023 04:06
              +1

              UPD к предыдущему комментарию. По результатам обсуждения Dispose для ICacheEntry лучше все-таки вызывать всегда — ибо есть шанс оставить ссылку на этот независимый элемент кэша в потенциально долгоживущем объекте текущего контекста выполнения (ExecutionContext).
              Что до ошибочного элемент кэша, то проще всего сделать установку свойства Value самой последней операцией перед Dispose: по исходному коду видно, что если Value==null то MemoryCache.SetEntry вызываться не будет, то и элемент в кэше тоже сохранен не будет. В крайнем случае, можно в перехватчике исключения удалить сбойный элемент через IMemoryCache.Remove: если элемента с таким ключом нет, то этот вызов будет просто проигнорирован.


          1. mvv-rus Автор
            04.09.2023 04:06
            -1

            Dispose сам по себе, при таком неочевидном кейсе не гарантирует ну никак отсутствие утечек памяти.
            Ага, Dispose тут выполняет совсем другую работу. В принципе, если объекты Key и Value не ссылаются на неуправляемые ресурсы (прямо или косвенно), то очистка тут вообще не требуется: сам MemoryCache неуправляемую память не использует. А если ссылаются, то вызов Dispose() для них надо писать самому: в процессе установки свойств, до добавленния — в обработчике исключения в try… catch, а после добавления в кэш — в eviction callback.


  1. syusifov
    04.09.2023 04:06
    -2

    дебилы, что еще сказать


  1. mikegordan
    04.09.2023 04:06
    -1

    Ну не знаю ребят насчет "неправильного использования" , всетаки такие изменения были .net core 2.1 для улучшения многопоточности , как я помню они убрал какие то локи в ситуации: когда внутри asp.net на запрос создавался системный поток , а потом пользователь внутри создавал свой поток без блокировки, и системный отпускался, отдавался Response клиенту и вызывался Dispose на МемориЧеч , ПРИ ЭТОМ Меморич использовался и внутри системного потока , так и внутри пользовательского потока (замыкание на мемчеч).