Ответ 1
Мне интересно, нужно ли вообще делать кэширование здесь, поскольку SQL-сервер довольно хорош в результатах кэширования, поэтому вы должны увидеть запросы, которые возвращают фиксированный список выпадающих значений, чтобы быть действительно быстрыми.
Конечно, когда вы выполняете кеширование, это зависит от данных о том, какова должна быть продолжительность кеша. Это зависит от того, как используется система. Например, если новые значения добавляются администратором, легко объяснить, что требуется несколько минут, прежде чем другие пользователи увидят его изменения.
Если, с другой стороны, обычный пользователь должен добавить значения, при работе с экраном, имеющим такой список, все может быть иначе. Но в этом случае, возможно, было бы неплохо сделать опыт для пользователя более свободным, представив его с раскрывающимся списком или предоставив ему возможность добавить новое значение прямо там. Это новое значение, чем обрабатывается в одной транзакции, и все будет хорошо.
Если вы хотите сделать недействительность кеша, я бы сказал, что вам нужно позволить вашим командам публиковать события домена. Таким образом, другие независимые части системы могут реагировать на эту операцию и могут (помимо всего прочего) сделать недействительность кэша.
Например:
public class AddCityCommandHandler : ICommandHandler<AddCityCommand>
{
private readonly IRepository<City> cityRepository;
private readonly IGuidProvider guidProvider;
private readonly IDomainEventPublisher eventPublisher;
public AddCountryCommandHandler(IRepository<City> cityRepository,
IGuidProvider guidProvider, IDomainEventPublisher eventPublisher) { ... }
public void Handle(AddCityCommand command)
{
City city = cityRepository.Create();
city.Id = this.guidProvider.NewGuid();
city.CountryId = command.CountryId;
this.eventPublisher.Publish(new CityAdded(city.Id));
}
}
Здесь вы публикуете событие CityAdded
, которое может выглядеть так:
public class CityAdded : IDomainEvent
{
public readonly Guid CityId;
public CityAdded (Guid cityId) {
if (cityId == Guid.Empty) throw ArgumentException();
this.CityId = cityId;
}
}
Теперь у вас может быть ноль или более подписчиков для этого события:
public class InvalidateGetCitiesByCountryQueryCache : IEventHandler<CityAdded>
{
private readonly IQueryCache queryCache;
private readonly IRepository<City> cityRepository;
public InvalidateGetCitiesByCountryQueryCache(...) { ... }
public void Handle(CityAdded e)
{
Guid countryId = this.cityRepository.GetById(e.CityId).CountryId;
this.queryCache.Invalidate(new GetCitiesByCountryQuery(countryId));
}
}
Здесь у нас есть специальный обработчик событий, который обрабатывает событие домена CityAdded
только для того, чтобы заблокировать кеш для GetCitiesByCountryQuery
. IQueryCache
здесь - абстракция, специально созданная для кэширования и аннулирования результатов запроса. InvalidateGetCitiesByCountryQueryCache
явно создает запрос, результаты которого должны быть недействительными. Этот метод Invalidate
может не использовать интерфейс ICachedQuery
для определения его ключа и недействительности результатов (если они есть).
Вместо использования ICachedQuery
для определения ключа, однако, я просто сериализую весь запрос в JSON и использую его как ключ. Таким образом, каждый запрос с уникальными параметрами автоматически получит свой собственный ключ и кеш, и вам не нужно реализовывать его в самом запросе. Это очень безопасный механизм. Однако, если ваш кеш должен пережить переработку AppDomain, вам необходимо убедиться, что вы получите точно такой же ключ для перезапуска приложений (что означает, что нужно упорядочить сериализованные свойства ).
Одна вещь, о которой вы должны помнить, заключается в том, что этот механизм особенно подходит в случае возможной последовательности. Чтобы взять предыдущий пример, когда вы хотите сделать недействительным кеш? Прежде чем добавить город или после? Если вы ранее недействили кеш, возможно, что кеш будет заселен до того, как вы сделаете фиксацию. Конечно, это будет сосать. С другой стороны, если вы это сделаете сразу после этого, возможно, что кто-то все еще наблюдает за старым значением непосредственно после. Особенно, когда ваши события помещаются в очередь и обрабатываются в фоновом режиме.
Но то, что вы можете сделать, - это выполнить очереди в очереди сразу после совершения фиксации. Для этого вы можете использовать декоратор обработчика команд:
public class EventProcessorCommandHandlerDecorator<T> : ICommandHandler<T>
{
private readonly EventPublisherImpl eventPublisher;
private readonly IEventProcessor eventProcessor;
private readonly ICommandHandler<T> decoratee;
public void Handle(T command)
{
this.decotatee.Handle(command);
foreach (IDomainEvent e in this.eventPublisher.GetQueuedEvents())
{
this.eventProcessor.Process(e);
}
}
}
Здесь декоратор напрямую зависит от реализации издателя события, чтобы разрешить вызов метода GetQueuedEvents()
, который был бы недоступен из интерфейса IDomainEventPublisher
. И мы повторяем все события и передаем эти события посреднику IEventProcessor
(который работает только как IQueryProcessor
).
Однако обратите внимание на некоторые вещи об этой реализации. Это НЕ транзакционно. Если вам нужно убедиться, что все ваши события обработаны, вам необходимо сохранить их в транзакционной очереди и обработать их оттуда. Однако для недействительности кеша это не кажется большой проблемой для меня.
Этот дизайн может показаться излишним только для кеширования, но как только вы начали публиковать события в домене, вы начнете видеть множество вариантов использования для них, что значительно упростит работу с вашей системой.