Модуль кэширования ASP.NET приложений

в 12:14, , рубрики: .net, ASP.NET, caching, метки: , , ,

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

После нескольких часов исследований были найдены следующие способы решения данной задачи:

1) Декларативное объявление директивы “OutputCache” на странице, которую необходимо кэшировать.

<%@ OutputCache Duration="600" VaryByParam="*" VaryByCustom="custom" %>

2) Установка параметров кэширования объекта Response.Cache в “coudeBehind” страницы:

    HttpCachePolicy policy = Response.Cache;
    policy.SetCacheability(HttpCacheability.Server);
    policy.SetExpires(app.Context.Timestamp.AddSeconds((double)600));
    policy.SetMaxAge(new TimeSpan(0, 0, 600));
    policy.SetValidUntilExpires(true);
    policy.SetLastModified(app.Context.Timestamp);
    policy.VaryByParams.IgnoreParams = true;

Оба данных подхода имеют существенные недостатки, такие как:

  • отсутствие гибкости
  • необходимость задавать параметры на каждой странице

Первое желание, которое возникло у меня при анализе задачи — создать универсальный модуль кэширования, чем мы сейчас и займемся.

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

Реализация модуля

Создадим класс “OutputCacheModule” реализующий интерфейс “IHttpModule”, и реализуем два метода “Dispose” и “Init”. В нашем случае метод “Dispose” можно оставить без реализации, а метод “Init” рассмотрим ближе.
Метод “Init” принимает “HttpApplication”, это позволяет получать доступ к глобальным событиям приложения, чем необходимо воспользоваться, подписавшись на несколько интересующих нас событий:

public void Init(HttpApplication app)
 {
     app.PreRequestHandlerExecute += new EventHandler(OnApplicationPreRequestHandlerExecute);
     app.PostRequestHandlerExecute += new EventHandler(OnPostRequestHandlerExecute);
 }

Метод “PostRequestHandlerExecute” вызывается сразу после того, как вызываемая страница или иной обработчик закончит обработку запроса. На этом этапе мы проверяем ряд условий и принимаем решение о необходимости кэширования и добавляем необходимые “headers”:

public void OnPostRequestHandlerExecute(object sender, EventArgs e)
      {
          HttpApplication app = (HttpApplication)sender;

		Int32 duration = CacheHelper.GetCacheDuration(app.Context.Request);
		
          HttpCachePolicy policy = app.Response.Cache;
          if (!InverseContext.User.IsAuthenticated && duration > 0)
          {
              policy.SetCacheability(HttpCacheability.Server);
              policy.SetExpires(app.Context.Timestamp.AddSeconds(duration ));
              policy.SetMaxAge(new TimeSpan(0, 0, duration ));
              policy.SetValidUntilExpires(true);
              policy.SetLastModified(app.Context.Timestamp);
              policy.VaryByParams["*"] = true;
          }
          else
          {
              policy.SetCacheability(HttpCacheability.NoCache);
              policy.SetExpires(app.Context.Timestamp.AddSeconds(0));
          }
  	}

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

А теперь вернемся к первому методу “PreRequestHandlerExecute”, который мы намеренно пропустили, чтоб не нарушать линейность повествования. Метод “PreRequestHandlerExecute” вызывается непосредственно перед тем, как вызываемая страница или иной обработчик приступит к выполнению запроса. И на этом этапе нам необходимо указать “callback” метод для проверки валидности страниц в нашем кэше:

  public void OnApplicationPreRequestHandlerExecute(object sender, EventArgs e)
      {
          HttpApplication app = (HttpApplication) sender;
          if (!InverseContext.User.IsAuthenticated)
          {
              app.Context.Response.Cache.AddValidationCallback(new HttpCacheValidateHandler(Validate), app);
          }
  	}

Эта запись значит буквально следующее — каждый раз, когда запрашивается страница, находящаяся в кэше, в первую очередь будет вызван “callback” метод, который проверит все необходимые условия и, либо вернет кэшированную версию страницы, либо инициирует новый цикл обработки страницы. Посмотрим на код этого метода:

public void Validate(HttpContext context, Object data, ref HttpValidationStatus status)
  	{
          if (InverseContext.User.IsAuthenticated)
          {
              status = HttpValidationStatus.IgnoreThisRequest;
              context.Response.Cache.AddValidationCallback(new HttpCacheValidateHandler(Validate), "somecustomdata");
          }
      	else
      	  {
              status = HttpValidationStatus.Valid;
      	  }
  	}

В данном примере реализовано очень простое условие, если пользователь не залогинен, то он получит страницу из кэша. Как можно заметить количество условий ограничивается только нашими потребностями. Но даже в этом методе есть аспект, на который следует обратить внимание, а именно:
“HttpValidationStatus” — определяет статус закэшированных данных, имеет три возможных значения:

  • IgnoreThisRequest — страница отработает заново, но версия лежащая в кэше останется валидной.
  • Invalid — страница отработает заново, версия в кэше будет признана не валидной и будет заменена на новую, отработанную в данном запросе.
  • Valid — кэш валиден, ответом будет версия страницы из кэша.

В период тестирования будет удобно получать служебную информацию о параметрах кэширования, которые реально были установлены для данной страницы. Для этого в методе “Init” следует дополнительно подписаться на событие “Application_EndRequest”, так как в этот момент все этапы обработки запроса уже завершились, и теоретически, никто не должен более менять “headers”. Для отображения информации можно использовать подобный метод:

private void PrintServiceInformation(HttpApplication app)
  	{
      	app.Context.Response.Write("Rendered at " + DateTime.Now + "<br/>");

      	var cacheability = app.Context.Response.Cache.GetType().GetField("_cacheability", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(app.Context.Response.Cache);
          app.Context.Response.Write("HttpCacheability = " + cacheability);

      	var expires = app.Context.Response.Cache.GetType().GetField("_utcExpires", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(app.Context.Response.Cache);
      	app.Context.Response.Write("<br/> UtcExpires = " + expires);
  	}

Без магии рефлекшена тут не обойтись. Но это только для этапа тестирования. В дальнейшем следует отказаться от использования этого метода.

Немного колдовства от Microsoft напоследок

1) Не следует использовать метод “Responce.Flush()” в местах, где предполагается использовать кэширование, так как данный метод при работе сайта под управлением IIS сервера вызывает метод “SetCacheability()” с параметром “HttpCacheability.Private”, а перетереть это нельзя (см пункт 2).

2) Параметры кэширования в ASP.Net нельзя перетереть, так как при попытке выставить “HttpCacheability” метод выполняет проверку

if (s_cacheabilityValues[(int)cacheability] < s_cacheabilityValues[(int)_cacheability])

А “enum” выглядит следующим образом:

public enum HttpCacheability
{
	NoCache = 1,
	Private = 2,
	Server = 3,
	ServerAndNoCache = 3,
	Public = 4,
	ServerAndPrivate = 5,
}

Так что невозможно поставить большее по «int» значение, чем то, что уже стоит. Аналогично работает метод “SetExpires”.

Автор: Nik5036350

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js