Практики регулирования доступа к кэшированным (редко изменяемым) данным в условиях многопоточности?
=====================================================================================================

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

Суть следующая:  

Имеется сервер, который в силу поставленных перед ним задач - очень простой, написан на базе обычного `HttpListener`. Средняя нагрузка (без критических задержек) составляет примерно 1000 запросов в секунду, при этом необходимая (заказчику) максимальная нагрузка должна составлять примерно 100 rps, т.е. запас "прочности" есть. Обработка запросов выполняется асинхронно. Для выдачи ответа на тот или иной запрос очень часто необходимо обращаться к данным, которые изначально хранятся в БД и которые можно назвать общими (shared), поскольку они (или их часть) в неизменном виде потребляется для целей формирования ответа на разные запросы. Поскольку структура всех данных и типы строго определены и не меняются, то при первой загрузке этих данных из БД они упаковываются в конкретные сущности, представленные в виде классов POCO. Разумеется, эти сущности кэшируются сервером с использованием простого `ConcurrentDictionary` и скользящим графиком сроков жизни его элементов. Длина ответов не более 300-500 байт, они тоже кэшируются, поскольку львиная доля запросов является повторяющимися, просто они приходят от разных клиентов. Все это работало очень шустро и хорошо в течение 5 лет.  
  

Не так давно возникла необходимость в ходе запросов не только читать те самые «расшаренные» данные, но и эпизодически их изменять. Сущности, которые представляют эти данные, хранятся в словарях (кэше), а значит, когда запрос берет сущность он тем самым берет не её, а указатель на её. Каждый класс, который представляет ту или иную сущность, имеет поля и свойства доступные только для чтения и есть только два способа, которые позволяют изменить значения полей – это функция обновить и метод удалить. Касаемо этих способов все понятно: для предотвращения конфликтов доступа к операциям обновления и удаления используются асинхронные блокировки в режиме записи.  

Но вот непонятно: как быть с полями и свойствами?, а именно: например, если в момент чтения того или иного свойства происходит обновление или удаление, то я должен оборачивать в блокировку режима чтения каждое поле/свойство?... а может вообще вся архитектура кеширования, исходя из новых потребностей, требует полого пересмотра?  
  

Существует смежная проблема: поля в некоторых сущностях являются списками (типа `IList<T>` и т.п.), когда будет происходить обновление данных и в частности содержимого списка (путём вставки и/или удаления элементов) и в этот момент кто-то будут проводить перебор его элементов, то сами понимаете, что произойдёт…  
  

Дополню реалиями: в силу некоторых причин заказчик чрезвычайно консервативен, я не могу использовать никакие сторонние библиотеки и кроме .NET Framework 4.7. (на который год назад было портировано решение) с его стандартным набором библиотек, В принципе для решения его задач они и не потребовались.  
  

Приветствуются любые комментарии, отсылки на практики (кроме Google), может изложение своего опыта, чтоб иметь возможность собрать все это в кучу и превратить в лаконичное решение. Спасибо. 

>  если в момент чтения того или иного свойства происходит обновление или удаление, то я должен оборачивать в блокировку режима чтения каждое поле/свойство?

  

Что-то это уже не похоже на кэш. Может быть, когда изменяется объект, то из кэша убирать его \_после\_ изменения. Тогда последующие потоки получат верный объект. А операции добавления и удаления в ConcurrentDictionary потокобезопасны. Т.е. идея в том чтобы не изменять объект в кэше никогда, а только удалять и добавлять.  
  

> Существует смежная проблема: поля в некоторых сущностях являются списками ...

  

Ну, теже ConcurrentQueue, СoncurrentStack поддерживают GetEnumerable, который делает копию всего списка. Тогда не страшно, что кто-то изменит список.  

Тогда можно без сторонних библиотек обойтись. Вообще, сложно что-то сказать конкретно без кода. 

 Если данные в кеше недействительны, то кэш сбрасывается и перечитывается по новой. Чтобы весь глобально не сбрасывать, его делят на слои и сбрасывают необходимый слой. Чтобы отдельно объект в кэше обновлять, такого не встречал.  
  

Если слои кэша в Concurrent списках, то без разницы, кто его в этот момент читает. Мы для кэша используем Lazy.  
  

Я так понимаю, что данные хоть и будут изменяться, то не часто, иначе нет смысла в кэше. В нашем случае, когда приходят новые данные или мы их обновляем, мы руками вызываем \_cache.Layer.Reset()