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

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

Дилемма: как и в каком объеме рассказывать

При написании статьи у меня возник вопрос о том, в каком объеме и на каком уровне рассказывать про функцию ограничения скорости обработки запросов на ASP.NET Core. Потому что раскрывать эту тему можно на разных уровнях.

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

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

И, наконец, можно описать код на уровне деталей реализации - рассказать словами, что делает каждый его кусок. Мне интересно копаться в коде именно на таком уровне: и с целью восстановить концептуальное описание, понять, что происходит в целом, и чтобы и подсмотреть интересные приемы программирования. Но если такое описание делать целиком, по всему коду, то оно получается очень громоздким и, к тому же - не особо интересным: большая часть кода устроена слишком просто, чтобы о нем было интересно рассказывать.Поэтому после того, как код разобран и его концептуальное описание сделано, описания, по моему мнению, заслуживают только найденные в коде интересные приемы. Впрочем, описание с целью привязки найденных концепций (абстрактных) к элементам кода (конкретным классам, полям/свойствам, методам и т.д.) тоже заслуживает описания - чтобы дать начальную информацию тем, кто хочет посмотреть что-то в этом коде.

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

Статья, которую вы сейчас читаете, содержит руководство по использованию функции ограничения скорости обработки входящих запросов к веб-приложению на базе ASP.NET Core. Она - первая из цикла статей, в котором рассказывается про функцию ограничения скорости обработки входящих запросов в ASP.NET Core.

Состав цикла
  1. Руководство по использованию функции ограничения скорости обработки входящих запросов в ASP.NET Core - эта статья.

  2. Универсальный компонент ограничения скорости в .NET.

  3. Устройство и работа классов базовых ограничителей.

  4. Компонент-обработчик ограничения скорости обработки запросов в ASP.NET Core.

UPD: Опубликовал вторую статью.

UPD: Опубликовал третью статью. Четвертая статьи цикла пока что лежит в черновиках в высокой степени готовности. Я планирую опубликовать ее в течение этой недели, но если вдруг кому-то захочется их почитать немедленно - пишите в коментариях: опубликую, как как есть. Не думаю, правда, что это целесообразно: эта статья представляет собой довольно большой массив довольно скучного текста, который имеет смысл читать чисто по возникновении потребности.

В этой статье написано, с примерами и лишь необходимым минимумом подробностей, как использовать функцию ограничения скорости обработки входящих запросов в ASP.NET Core в своих программах. Она рассчитана на то, чтобы приносить тем самым простую, утилитарную пользу без лишних заморочек. А кому станет интересно, а как же эта функция устроена и работает, и почему в руководстве написано так, а не иначе - милости прошу читать цикл дальше. В следующих статьях цикла я буду рассказывать, из чего сделана и как работает на концептуальном уровне эта функция, а в скрытом тексте в тех статьях - поделюсь наиболее интересными деталями реализации. Кроме того, в приложениях к этим следующим статьям я приведу примеры реализации некоторых потенциально полезных возможностей с помощью нестандартного использования рассматриваемой функции.

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

Блоки скрытого текста в этой статье...

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

Содержание

Введение Добавление функции ограничения запросов в приложение ASP.NET Core

Настройка параметров ограничения запросов.

Ограничение запросов и точки назначения маршрутизации.

Заключение. Бонус-приложение: за пределами фирменного руководства. Безымянные политики ограничения запросов.

Введение

Итак, в этой статье я ограничусь только тем, как использовать функцию ограничения запросов из ASP.NET Core в своем веб-приложении.

Тут полагается написать общие слова, зачем нужно использовать ограничение скорости обработки запросов...

… но писать самому не вижу смысла: раз уж читатель заинтересовался этой статьей, то он эти слова уже, скорее всего, видел. Но, для порядка, процитирую те общие слова, которые написаны на этот счет в документации самой Microsoft (я тут взял на себя смелость поправить оригинальный перевод с сайта):

Основные причины реализации ограничения скорости:

  • предотвращение злоупотреблений: ограничение скорости помогает защитить приложение от злоупотреблений, ограничив количество запросов, которые пользователь или клиент могут выполнять в течение заданного периода времени, это особенно важно для общедоступных API;

  • обеспечение справедливого использования: путем установки ограничений все пользователи имеют справедливый доступ к ресурсам, это предотвращает монополизацию системы пользователями;

  • защита ресурсов: ограничение скорости помогает предотвратить перегрузку сервера, контролируя количество обрабатываемых запросов, защищая таким образом внутренние ресурсы от перегрузки;

  • повышение безопасности: можно снизить риск атак типа “отказ в обслуживании” (DoS), ограничив скорость обработки запросов, затрудняя злоумышленникам перегрузку системы;

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

  • управление расходами: для услуг, стоимость которых зависит от использования, ограничение скорости может помочь управлять и прогнозировать расходы за счет контроля объема обрабатываемых запросов.

Технически ограничение скорости обработки запросов реализуется в компоненте-обработчике (middleware) устанавливаемом в конвейер обработчиков приложения.

Добавление функции ограничения запросов в приложение ASP.NET Core

Для использования функции ограничения запросов в проекте ASP.NET Core никаких дополнительных зависимостей добавлять в проект не требуется - все нужные зависимости уже указаны в SDK, на котором основан проект. Требуется только настроить приложение на использование ограничителя. Для этого нужно сделать два предварительных действия.

Действие первое: настройка контейнера сервисов для функции ограничения запросов

В современных приложениях на базе WebApplication для этого нужно вызвать метод расширения AddRateLimiter для WebApplicationBuilder.Services. Обычно он вызывается с одним дополнительным параметром - делегатом, который используется для настройки параметров ограничения запросов (она описана дальше в руководстве).

var builder = WebApplication.CreateBuilder(args);
//...
builder.AddRateLimiter( options => {
    // Здесь в options производится настройка параметров компонента-обработчика ограничения запросов.
});
О способах настройки параметров ограничения запросов.

Обычно настройка параметров ограничения запросов делается делегатом, передаваемым как аргумент в метод AddRateLimiter. Внутри этот делегат производит установку свойств объекта параметров настройки ограничения, внедряемого как параметр-зависимость в конструктор компонента-обработчика ограничения запросов из контейнера сервисов с использованием Options pattern).

Но использование именно такого способа установки параметров ограничения необязательно. Во-первых, существует ещё несколько способов настройки экземпляра класса параметров для передачи из контейнера сервисов через Options pattern. А во-вторых, есть сценарии, в которых настройка этих параметров либо вообще не требуется (для использования только безымянных политик, см. Приложение), либо параметры настройки ограничения передаются не путем внедрения зависимости через Options pattern, а напрямую (см. следующий пункт). Для таких случаев, начиная с ASP.NET Core 9, появилась другая версия метода AddRateLimiter - без дополнительного параметра-делегата.

builder.AddRateLimiter();

Но метод AddRateLimiter должен (начиная с ASP.NET Core 8) вызываться в любом случае: он, кроме настройки параметров ограничения, ещё и добавляет в контейнер сервисов и другие сервисы, от которых зависит компонент-обработчик ограничения запросов.

Действие второе: добавление компонента-обработчика (middleware) ограничения запросов в конвейер приложения.

Для этого используется метод расширения UseRateLimiter для объекта WebApplication (технически - для реализуемого им интерфейса построителя приложений IApplicationBuilder). Есть две формы этого метода. Первая - без дополнительных параметров:

var app=builder.Build();
//...
app.UseRateLimiter();

При использовании этой формы для настройки функции ограничения запросов используются параметры настройки ограничения запросов из контейнера сервисов, установленные делегатом, переданным в AddRateLimiter (или - другим способом настройки Options pattern).

Во вторую форму метода UseRateLimiter через дополнительный параметр передаются особые параметры настройки ограничения запросов, а параметры из контейнера сервисов, устанавливаемые делегатом, передаваемым в AddRateLimiter, не используются:

var app=builder.Build();
//...
var options=new RateLimiterOptions();
// Здесь в options производится настройка параметров компонента-обработчика ограничения запросов.
app.UseRateLimiter(options);

Настройка параметров ограничения запросов.

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

Алгоритмы ограничения запросов

Алгоритм ограничения запросов - это базовое понятие, описывающее, что именно ограничивается, и как именно производится ограничение. Для каждого алгоритма в .NET определен класс, экземпляр которого отслеживает текущее состояние ограничения и выдает либо отклоняет разрешение компоненту ограничителя запросов на обработку текущего запроса.

В .NET (и соответственно в ASP.NET Core) реализованы четыре алгоритма ограничения запросов. Три первых из них ограничивают скорость обработки запросов в приложении. Отличаются они тем, как именно принимается решение о выдаче разрешения.

  1. Ограничение с фиксированным окном. Шкала времени разбивается на интервалы фиксированной продолжительности (окна), и в каждом окне допускается выдать только указанное количество разрешений. После перехода к следующему окну подсчет выданных разрешений производится заново, с нуля.

  2. Ограничение со скользящим окном. Аналогично предыдущему алгоритму, позволяет выдать в окне не более указанного количества разрешений, но окно, для которого подсчитываются выданные разрешения, перемещается таким образом, чтобы его конец совпадал с текущим моментом времени. В реализации .NET смещение окна производится не непрерывно, а в дискретные моменты времени: вся длительность окна разбивается на некоторое количество сегментов одинакового размера и перемещение происходит только когда текущий момент сдвигается от конца старого окна больше, чем на размер сегмента.

  3. Ограничение с пополняемой емкостью для жетонов (token bucket, “алгоритм дырявого ведра”). Алгоритм оперирует с воображаемой емкостью, в которое находится некоторое количество жетонов, от нуля до их предельного количества. Для получения каждого разрешения требуется забрать один жетон из емкости. Если жетонов в емкости в какой-то момент нет, то разрешение в этот момент выдать невозможно. Емкость пополняется с постоянной скоростью, через указанные промежутки времени указанным количеством жетонов вплоть до их предельного количества.

Четвертый реализованный в .NET алгоритм ограничения - ограничение одновременного использования - при ограничении запросов из ASP.NET Core ограничивает количество одновременно обрабатываемых запросов, поэтому его часто также называют алгоритмом ограничения параллелизма. Этот алгоритм выдает разрешение на обработку следующего запроса, только если выдача разрешения не приведет к превышению предельного количества одновременно обрабатываемых запросов.

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

Селективность ограничения запросов.

Компонент-обработчик ограничения запросов, как правило, не ограничивает все запросы к приложению скопом, применяя к ним одно ограничение. Вместо этого делит запросы на группы, в зависимости от параметров, содержащихся в контексте запроса (HttpContext) и для каждой такой группы применяет отдельное ограничение. Таким образом, ограничение запросов в ASP.NET Core является селективным, а реализующий его объект логично называть селективным ограничителем.

Использование базовых ограничителей селективным ограничителем

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

Стоит также упомянуть, что для каких-то групп запросов селективный ограничитель может не применять ограничение вовсе.

Для тех кто планирует читать скрытые тексты дальше...

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

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

Глобальный ограничитель и политики ограничения запросов.

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

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

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

Настройка глобального ограничителя.

При настройке глобальный ограничитель указывается в свойстве GlobalLimiter объекта параметров настройки ограничения запросов. Это свойство может содержать ссылку на селективный ограничитель который и будет глобальным ограничителем, а может быть пустым (null), если глобальный ограничитель не устанавливается.

Группировка ограничиваемых запросов по одному ключу.

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

builder.Services.AddRateLimiter(options => {
    options.GlobalLimiter = PartitionedRateLimiter.Create(
        (HttpContext httpContext) => RateLimitPartition.GetFixedWindowLimiter(
            httpContext.User.Identity?.Name ?? "",
            _ => new FixedWindowRateLimiterOptions { PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }
        )
    );
});

Пояснения к примеру.

  1. Используемый алгоритм ограничения определяется вспомогательным методом, вызываемом в делегате, переданном как аргумент в метод PartitionedRateLimiter.Create. В данном примере используется статический метод GetFixedWindowLimiter класса RateLimitPartition, позволяющий использовать базовый ограничитель, выполняющий алгоритм ограничения с фиксированным окном.

  2. Ключ группировки - значение из контекста запроса, используемое для выбора группы - указано в первом аргументе этого метода. В данном случае ключом является имя пользователя (или пустая строка, если пользователь не был указан ). Для использования другого ключа нужно указать другое выражение, которое получает ключ из контекста запроса.

  3. Второй аргумент этого метода - делегат, возвращающий параметры для настройки (типа FixedWindowRateLimiterOptions) создаваемого базового ограничителя реализующего алгоритм с фиксированным окном.

Похожий пример из руководства Microsoft - неудачный

Данный пример - это доработанный пример из этого раздела руководства Microsoft. Недостатков у оригинального примера по ссылке два. Во-первых, для стрелочной(лямбда) функции, являющейся аргументом метода PartitionedRateLimiter.Create, не указан тип параметра httpContext. В результате компилятор не может вывести параметры-типы этого обобщенного метода, и их приходится указывать явно. Это недостаток небольшой, чисто эстетический, на работу кода не влияющий. Второй недостаток - более существенный: в параметрах настройки FixedWindowRateLimiterOptions, которые создаются делегатом - вторым аргументом в метод GetFixedWindowLimiter, поле AutoReplenishment устанавливается в true. Это неправильно, потому что метод GetFixedWindowLimiter (и аналогичные ему методы для других алгоритмов из списка ниже) сбрасывают это поле параметров настройки в false. Зачем AutoReplenishment сбрасывается в false - про это рассказывается в следующей статье цикла, а почему его нужно сбрасывать - далеко не очевидно для человека, не знакомого с внутренностями этого ограничителя скорости. Причем, для изменения поля AutoReplenishment в куче создается дополнительная копия объекта параметров, потому что менять объект параметров, возвращаемый делегатом, переданным через параметр, код метода GetFixedWindowLimiter не имеет права: делегат, в принципе, может возвратить и ссылку на некий разделяемый объект (в данном примере это не так, но…). То есть, в примере из документации возникают лишние, совершенно ненужные размещения объектов в куче и лишняя нагрузка на сборщик мусора. Куда лучше было бы сразу установить поле AutoReplenishment параметров настройки алгоритма в false. По крайней мере, уж в примере из руководства, код из которого явно будут брать за основу и копировать в свои программы многочисленные пользователи, стоило бы сделать именно так.

Для использование других алгоритмов в классе RateLimitPartition определены другие методы, использующие другие типы для своих параметров настройки:

  • GetSlidingWindowLimiter для алгоритма скользящего окна, его тип параметров настройки - SlidingWindowRateLimiterOptions;

  • GetTokenBucketLimiter для алгоритма с пополняемой емкостью для жетонов, его тип параметров настройки - TokenBucketRateLimiterOptions;

  • GetConcurrencyLimiter для алгоритма ограничителя одновременного использования (параллелизма), его тип параметров настройки - ConcurrencyLimiterOptions;

  • GetNoLimiter для безусловной выдачи разрешения, параметры настройки для него не требуются.

То есть, для использования другого алгоритма нужно заменить GetFixedWindowLimiter на название метода для этого алгоритма, тип параметров настройки FixedWindowRateLimiterOptions - на название нужного типа, и указать в его инициализаторе нужные значения для этих параметров. Примеры использования можно посмотреть в документации, в описания алгоритмов, по тем же приведенным ранее в списке алгоритмов ссылкам.

Что здесь происходит и почему всё так сложно

Что здесь происходит, сказать просто: тут настраивается создание секционированного ограничителя, который будет служить глобальным ограничителем запросов. А всё так сложно, потому что разные части кода, составляющего пример, работать будут не обязательно сейчас, в момент вызова метода AddRateLimiter. Некоторые из них будут вызываться позже, в определенные последующие моменты выполнения программы. Именно поэтому в примере мы видим целых три вложенных друг в друга делегата (оформленных в виде лямбда-выражений). А на самом деле, делегатов тут четыре: ещё один делегат - часть результата, возвращаемого методом GetFixedWindowLimiter - мы не видим. А он есть.

Так вот, в примере выше в делегате настройки параметров ограничителя (самый внешний делегат) создается объект секционированного ограничителя, ссылка на который сохраняется в свойстве GlobalLimiter параметров настройки ограничителя - экземпляре класса RateLimiterOptions. Происходит это в момент построения конвейера, когда создается и добавляется в конвейер компонент-обработчик ограничителя запросов, с использованием механизмов внедрения зависимостей и шаблона параметров(options pattern).

Объект секционированного ограничителя создается вызовом статического метода PartitionedRateLimiter.Create, в который в качестве аргумента передается второй делегат - в следующих статьях цикла он называется секционирующим делегатом, там объяснено, откуда взялось такое название. Этот второй делегат сохраняется во внутреннем поле объекта секционированного ограничителя и затем вызывается методами этого объекта, через которые компонент ограничения запросов получает разрешение на обработку запроса. Второй делегат на основе содержимого контекста запроса (HttpContext) возвращает структуру, одно из полей которой - тот самый делегат, который мы не видим, а он есть (второе поле в этой структуре - значение ключа секционирования). Этот делегат создает на основе значения ключа секционирования базовый ограничитель, выполняющий заданный алгоритм ограничения.

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

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

Комбинирование селективных ограничителей запросов.

Существует возможность объединить два и более селективных ограничителя запросов в один комбинированный (иначе - сцепленный) селективный ограничитель. Этот ограничитель будет выдавать разрешение на выполнение, только если такое разрешение будет получено от каждого из комбинируемых ограничителей. Пример:

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create((HttpContext httpContext) => 
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, options =>
                new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 4,
                    Window = TimeSpan.FromSeconds(2)
                });
        }),
        PartitionedRateLimiter.Create((HttpContext httpContext) => 
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();
            
            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, options =>
                new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 20,    
                    Window = TimeSpan.FromSeconds(30)
                });
        }));
});

В примере мы видим настройку комбинированного (иначе - сцепленного) селективного ограничителя, ограничивающего запросы с одинаковым значением заголовка запроса User-Agent двумя ограничителями с фиксированным окном: первый разрешает посылку не более 4 запросов в окне в 2 секунды, второй - не более 20 запросов в полминутном окне.

Моя рекомендация по порядку комбинирования ограничителей

Я рекомендую при создании сцепленного селективного ограничителя комбинировать ограничители в порядке от наименее ограничивающего ограничителя (с наименее дефицитными разрешениями) к наиболее ограничивающему. В частности, если в комбинации используется алгоритм ограничения параллелизма - ставить ограничитель параллелизма в конец последовательности комбинируемых ограничителей. Связана такая рекомендация с тем, что текущая реализация сцепленного ограничителя получает разрешения от скомбинированных ограничителей последовательно, один за другим, и при этом уже полученные ранее разрешения удерживаются до окончания процесса получения всех разрешений. И если запрос к очередному ограничителю ставится в очередь, то все полученные до этого разрешения удерживаются, хотя фактически они не используются. Это особенно сильно влияет при использовании ограничителя параллелизма: для него число доступных разрешений увеличивается только в момент освобождения предыдущих полученных разрешений, в то время как для остальных алгоритмов ограничения, по скорости, число доступных разрешений со временем пополняется.

Новая возможность: комбинирование неселективных ограничителей.

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

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create((HttpContext httpContext) => 
    {
        var userAgent = httpContext.Request.Headers.UserAgent.ToString();
		
        return new RateLimitPartition(userAgent, _ => RateLimitPartition.CreateChained(
			new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions() {
					PermitLimit = 4,
					Window = TimeSpan.FromSeconds(2),
					AutoReplenishment = true
				}),
			new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions() {
					PermitLimit = 20,    
					Window = TimeSpan.FromSeconds(30),
					AutoReplenishment = true
				})
        ));
    });
});

Сцепленные неселективные ограничители можно использовать везде, где можно использовать и остальные базовые алгоритмические ограничители, в частности - для создания селективных ограничителей с группировкой по одному ключу или политик ограничения. Однако комбинировать неселективные ограничители с разными алгоритмами и/или наборами параметров для создания глобального ограничителя или политики ограничения получится, только если в этот селективный ограничитель или политика должны использоваться для ограничения всеми их составными частями групп, выделяемых по одному и тому же ключу. Если же требуется для разных алгоритмов и/или наборов параметров группировать запросы по разным ключам, то необходимо использовать комбинирование селективных ограничителей, как описано в предыдущем пункте: комбинирование неселективных ограничителей сделать это не позволит.

Внимание. Если вы собираетесь использовать сцепленный неселективный ограничитель как часть глобального ограничителя или политики ограничения, и в состав этого сцепленного ограничителя входят ограничители скорости обработки запросов (FixedWindowRateLimiter/SlidingWindowRateLimiter/TokenBucketRateLimiter), убедитесь, что эти ограничители создаются в режиме автоматического пополнения разрешений (свойство AutoReplenishment в их параметрах настройки устанавливается в true): сцепленный неселективный ограничитель не умеет вызывать пополнение разрешений для входящих в его состав ограничителей скорости в ручном режиме.

Эта новая возможность - комбинирование неселективных ограничителей - полезна, в частности, для создания сложных политик ограничения обработки запросов в ASP.NET Core, рассмотренных в четвертой статье цикла: в политиках ограничения нет штатной возможности использовать селективные ограничители (впрочем, в приложении к упомянутой статье рассказано, как эту возможность все-таки можно добавить).

Моя рекомендация по выбору порядка комбинирования ограничителей из предыдущего пункта остается в силе и для комбинирования неселективных ограничителей.

Настройка помещения запросов в очередь.

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

Для всех алгоритмических ограничителей, реализованных в .NET, постановка в очередь настраивается через два свойства в параметрах настройки с одними и теми же именами для всех алгоритмов: QueueLimit указывает максимальную длину очереди (0 - постановка в очередь не производится), QueueProcessingOrder - порядок извлечения из очереди при появлении свободных разрешений: OldestFirst - извлекается первый поставленный в очередь запрос, NewestFirst - последний поставленный в очередь (т.е. очередь фактически является стеком). Пример настройки для глобального ограничителя:

builder.Services.AddRateLimiter(options => {
    options.GlobalLimiter = PartitionedRateLimiter.Create(
        (HttpContext httpContext) => RateLimitPartition.GetFixedWindowLimiter(
            httpContext.User.Identity?.Name ?? "",
            _ => new FixedWindowRateLimiterOptions { 
                PermitLimit = 10, QueueLimit = 5, Window = TimeSpan.FromMinutes(1), QueueProcessingOrder = QueueProcessingOrder.OldestFirst 
            }
        )
    );
});

Этот пример - слегка измененный пример из раздела “Группировка по одному ключу.”. Отличается он тем, что здесь настроено помещение в очередь до 5 запросов, которые иначе были бы отклонены, и запросы из очереди извлекаются для получения разрешения в порядке их поступления.

Настройка действий при отклонении запроса.

Можно указать, что именно будет делать компонент-обработчик с запросом, разрешение на выполнение которого отклонено. Прежде всего - это какой код статуса HTTP будет установлен в ответе (обычно для этого используется статус 429 “To many requests”). Кроме того, можно указать функцию обратного вызова - делегат, выполняющий при отклонении какое-то действие. Делегат, в принципе, можно указать на двух уровнях - глобально и в политике (второй вариант в этой статье не рассматривается, он будет описан при подробном рассмотрении политик ограничения в одной из последующих статей цикла). Если при обработке запроса оказываются указаны оба делегата, то вызываться будет только делегат из политики.

Примеры глобальной настройки действий при отклонении запросов:

1.Простейший вариант - установка возвращаемого кода статуса

builder.Services.AddRateLimiter(options => {
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    // ... здесь производится дальнейшая настройка
});

2.Более сложный вариант - регистрация функции обратного вызова для установки в возвращаемом ответе кода статуса, заголовка Retry-After и текста сообщения для пользователя.

builder.Services.AddRateLimiter(options => {

    options.OnRejected = async (context, cancellationToken) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers["Retry-After"] = "90";
        await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.", cancellationToken);
    };
    // ... здесь производится дальнейшая настройка
});

Добавление именованных политик ограничения запросов

Простейший способ добавления именованных политик ограничения.

Простейший способ добавления политики (в руководстве от Microsoft показан только он) - использование вспомогательных методов расширения для класса параметров настройки ограничителя RateLimiterOptions: AddFixedWindowLimiter, AddSlidingWindowLimiter, AddTokenBucketLimiter, AddConcurrencyLimiter. Эти методы добавляют в параметры настройки ограничителя именованные политики, основанные, соответственно, на алгоритмах ограничения с фиксированным окном, ограничения со скользящим окном, ограничения с пополняемой емкостью для жетонов и ограничения параллелизма. Первый дополнительный параметр этих методов - строка, являющаяся именем политики, второй - делегат, устанавливающий параметры для настройки алгоритмического ограничителя для этой политики. Классы, содержащие параметры для каждого из алгоритмов, были рассмотрены ранее в описании простейшего варианта настройки глобального ограничителя.

Пример добавления в параметры настройки ограничения запросов политики с именем “fixed”, основанной на алгоритме ограничения с фиксированным окном:

builder.Services.AddRateLimiter(options =>{
    options.AddFixedWindowLimiter("fixed", opts =>
    {
        opts.PermitLimit = 20;
        opts.Window = TimeSpan.FromSeconds(60);
        opt.QueueLimit = 0;
    });
});

Добавляемая политика ограничения использует алгоритм с фиксированным окном длительностью 60 секунд и предельным количеством обрабатываемых запросов в рамках окна 20.

Ограниченность возможностей политик, добавленных простейшим способом

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

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

Добавление более сложных именованных политик ограничения.

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

В руководстве от Microsoft эти методы не рассматриваются, но я здесь приведу пример использования простейшего из этих методов. В этом примере в параметры настройки ограничения запросов добавляется политика с именем “fixed”, основанная на алгоритме ограничения с фиксированным окном и выполняющая селективное ограничение скорости запросов - отдельное для каждого пользователя:

builder.Services.AddRateLimiter(options => {
    options.AddPolicy("fixed",
        (HttpContext httpContext) => RateLimitPartition.GetFixedWindowLimiter(
            httpContext.User.Identity?.Name ?? "",
            _ => new FixedWindowRateLimiterOptions { AutoReplenishment = false, PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }
        )
    );
});

Этот пример добавляет политику, выполняющую селективное ограничение - группы запросов от разных пользователей ограничиваются независимо. Это ограничение полностью аналогично по алгоритму (алгоритм фиксированного окна) и по принципу разбиения запросов на группы (по пользователям) тому, которое выполняет глобальный ограничитель из примера в разделе “Группировка по одному ключу.”, потому что вторым аргументом в AddPolicy передается точно такой же делегат, как и в метод PartitionedRateLimiter.Create в том примере. Но, в отличие от глобального ограничителя, который применяется ко всем запросам, ограничение из этой политики будет применяться только к запросам, точка назначения которых будет связана с этой политикой.

Подробности

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

Чтобы указать в политике функцию обратного вызова для обработки отклонения запроса следует использовать другие варианты метода AddPolicy. Они требуют в качестве параметра класс (или его экземпляр), реализующий интерфейс политики ограничения. В данном руководстве этот интерфейс не рассматривается: он является предметом рассмотрения одной из последующих статей цикла, описывающей реализацию ограничения скорости обработки запросов в ASP.NET Core. Однако простейший пример класса, реализующего этот интерфейс (подробно этот класс описан приложении к упомянутой статье), можно найти и в этом руководстве, в бонус-приложении, посвященном безымянным политикам.

Ограничение запросов и точки назначения маршрутизации.

Функция ограничения скорости запросов ASP.NET Core позволяет отдельно указать для каждой точки назначения маршрутизации (оконечного обработчика запросов), как производить ограничение скорости направленных к ней запросов. Возможны два варианта указания применения: во-первых, с каждой точкой назначения можно связать используемую для нее политику ограничения запросов, а во-вторых, для конкретной точки назначения можно вообще отключать какое-либо ограничение скорости запросов.

Связывание политик ограничения запросов с точками назначения маршрутизации.

Связать политику ограничения запросов с точкой назначения можно двумя способами. Первый способ: для фреймворков, поддерживающих маршрутизацию на основе атрибутов (MVC, Razor Pages, MVC API Controllers) можно использовать для этого атрибут [EnableRateLimiting]. Этот атрибут имеет один аргумент - имя политики, под которым эта политика была добавлена в параметры настройки ограничения запросов. Атрибут можно использовать везде, где допустимо применение атрибутов маршрутизации и ее метаданных. Если атрибуты используются в нескольких местах, например, в MVC - для класса контроллера и конкретного метода действия в нем, то действует обычное правило приоритета для атрибутов маршрутизации - в примере ниже атрибут метода действия будет иметь приоритет выше, чем атрибут класса контроллера.

Пример использования атрибута [EnableRateLimiting]:

//File: program.cs
//...
var builder = WebApplication.CreateBuilder(args);
//...

builder.Services.AddRateLimiter(options => options.AddFixedWindowLimiter("fixed", opts => { /*...*/ }));
builder.Services.AddRateLimiter(options => options.AddSlidingWindowLimiter("sliding", opts => { /*...*/ }));
//...

//File: MyController.cs
[EnableRateLimiting("fixed")]
public class MyController : Controller
{
    //...
    public ActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("sliding")]
    public ActionResult SlidingAction()
    {
        return View();
    }

}

В этом примере, для ограничения запросов к точке назначения Index будет применяться политика “fixed”, а к точке назначения SlidingAction - политика “sliding”.

Второй вариант: использовать метод расширения RequireRateLimiting для классов, реализующих интерфейс IEndpointConventionBuilder, используемый для настройки метаданных маршрутизации. Экземпляры классов, реализующих этот интерфейс, возвращаются методами, устанавливающими маршруты к точкам назначения, имена этих методов обычно имеют вид “MapXXXX”. При вызове метода RequireRateLimiting с именем политики в качестве аргумента , в метаданные для всех точек назначения, настраиваемых через экземпляр класса, для которого вызывается этот метод (то есть - возвращенный методом MapXXXX), будет добавлена политика с этим именем. Если метод MapXXXX устанавливает точки назначения для фреймворка с маршрутизацией на основе атрибута, то установленная с помощью RequireRateLimiting политика заменит политики, указанные в атрибутах [EnableRateLimiting] точек назначения.

Пример использования метода RequireRateLimiting:

//...
var builder = WebApplication.CreateBuilder(args);
//...

builder.Services.AddRateLimiter(options => options.AddFixedWindowLimiter("fixed", opts => { /*...*/ }));

var app = builder.Build();
app.UseRateLimiter();
//...
app.MapDefaultControllerRoute().RequireRateLimiting("fixed");
//...

В этом примере для всех точек назначения - методов действий контроллеров MVC (для которых не отменено ограничение запросов, см. следующий пункт) будет применяться именованная политика “fixed”, а любые политики, назначенные с помощью атрибутов [EnableRateLimiting], для них применяться не будут

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

Отмена ограничения запросов к точками назначения маршрутизации.

Можно отменять ограничение запросов к некоторым точкам назначения маршрутизации. Аналогично назначению политик, отменять ограничение можно либо с помощью атрибута - [DisableRateLimiting], либо с помощью метода расширения DisableRateLimiting для интерфейса настройки метаданных маршрутизации IEndpointConventionBuilder. Отмена ограничения запросов имеет более высокий приоритет, чем назначение политики запросов. Более того, она отменяет применение не только политик ограничения запросов, но и применение к этой точке назначения глобального ограничителя запросов.

Пример отмены ограничения скорости обработки запросов с помощью атрибута [DisableRateLimiting]:

[EnableRateLimiting("fixed")]
public class MyController : Controller
{
    //...
    public ActionResult Index()
    {
        return View();
    }

    [DisableRateLimiting]
    public ActionResult Unconstrained()
    {
        return View();
    }

}

В этом примере при обращении к методу действия Index применяется политика ограничения “fixed”, настроенная на уровне класса контроллера, а при обращении к методу действия Unconstrained никакое ограничение запросов не применяется - даже то, которое было настроено с использованием глобального ограничителя запросов.

И о метриках

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

Заключение.

Вот и всё, о чем я хотел написать в руководстве. Минимума практических приемов-ритуалов, перечисленных в этой статье-руководстве, обычно достаточно для типового использования ограничения скорости обработки запросов в веб-приложении на базе ASP.NET Core. По крайней мере, в руководстве от Microsoft рассмотрены только эти приемы (и, кстати, не все из них).

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

Бонус-приложение: за пределами фирменного руководства. Безымянные политики ограничения запросов.

В этой статье я не смог пройти мимо одной возможности, о которой в руководстве от MS не упомянуто, но которая может быть весьма полезна для определенных сценариев использования функции ограничения скорости выполнения запросов в ASP.NET Core. А именно - безымянных политик ограничения запросов.

Как и когда использовать безымянную политику ограничения запросов.

Метод расширения RequireRateLimiting для класса, реализующего интерфейс IEndpointConventionBuilder, имеет ещё одну, вторую форму. В этой форме его дополнительный аргумент содержит не строку - имя политики, а саму политику. Эта форма метода RequireRateLimiting как раз и создает ту самую безымянную политику и реализует сохранение ее напрямую в метаданные маршрутизации точки назначения (или группы точек назначения, к интерфейсу IEndpointConventionBuilder для настройки которой был применен этот метод). Позднее, когда компонент-обработчик ограничения запросов будет обрабатывать запрос к этой точке назначения, он извлечет из метаданных и применит созданную безымянную политику. Таким образом, для использования безымянной политики ограничения запросов не требуется добавлять ее в параметры настройки ограничителя запросов, и это упрощает настройку приложения.

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

Почему и когда безымянные политики не изолированы

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

Так что правило для беспроблемного использования безымянных политик - в приложении не должно использоваться более одной такой политики.

Но нет правил без исключений

Можно вполне безопасно использовать несколько безымянных политик, если каждая из них имеет собственный тип ключа секционирования, причем значения разных типов при этом гарантированно не могут быть равны (о ключах секционирования см. далее в скрытом тексте в этом разделе и, более подробно - в описании работы секционированного ограничителя в следующей статье цикла). Если это правило соблюдается, то конфликт между такими политиками становится невозможен. Но на это IMHO лучше не закладываться - тип ключа секционирования, как и всё в коде, имеет свойство меняться при дальнейшей разработке и поддержке кода, а потому при использовании нескольких безымянных политик возникает потенциальная возможность возникновения совершенно непредусмотренной связи между ними, то есть, приложение получает дополнительную хрупкость.

А ещё можно совершенно безопасно использовать безымянные политики, сделанные на базе селективного ограничителя по методу, изложенному в приложении 2 к четвертой статье цикла - используемые в них суррогатные ключи секционирования гарантированно уникальны в рамках всего веб-приложения.

Для приложений, широко использующих политики ограничения скорости запросов, соблюдать это правило затруднительно. Но если в приложении ограничение скорости запросов используется только для одной конкретной точки назначения, то в таком приложении вполне разумно просто эту точку назначения связать с безымянной политикой, а общую настройку параметров ограничения запросов не делать: вызвать метод AddRateLimiter() без дополнительных аргументов (или, в версиях ASP.NET Core до 9-й - с пустым делегатом в качестве аргумента: AddRateLimiter(_=>{}) ). Именно в таких приложениях и стоит применять безымянную политику ограничения запросов.

Как реализовать безымянную политику ограничения запросов.

В качестве безымянной политики в метод RequireRateLimiting передается ссылка на экземпляр класса, реализующего интерфейс политики ограничения IRateLimiterPolicy.

Об интерфейсе IRateLimiterPolicy

Это - обобщенный интерфейс, имеющий один параметр-тип TPartitionKey, который содержит тип используемого в политике ключа секционирования (о котором было упомянуто в скрытом тексте выше). Он имеет один метод, GetPartition, возвращающий нужные для реализации политики данные (значение ключа секционирования и делегат, создающий ограничитель, используемый для группы с этим ключом, об этих данных (“данных для секции”) подробнее рассказано в статье цикла, где описаны политики ограничения), и одно свойство, OnRejected, которое, если оно установлено, содержит делегат, который выполняет специфическое для этой политики действие при отклонении запроса взамен настроенного на глобальном уровне действия по умолчанию.

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

Код класса DelegatedRateLimiterPolicy, реализующего интерфейс IRateLimiterPolicy

Примечание: этот код я также выложил и на свой GitHub.

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

namespace MVVRus.AspNetCore.RateLimiting
{
    public class DelegatedRateLimiterPolicy<TPartitionKey> : IRateLimiterPolicy<TPartitionKey>
    {
        Func<HttpContext, RateLimitPartition<TPartitionKey>> _partitioner;
        public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; }

        public DelegatedRateLimiterPolicy(Func<HttpContext, RateLimitPartition<TPartitionKey>> partitioner, 
            Func<OnRejectedContext, CancellationToken, ValueTask>? onRejected = null)
        {
            _partitioner = partitioner;
            OnRejected=onRejected;
        }

        public RateLimitPartition<TPartitionKey> GetPartition(HttpContext httpContext)
        {
            return _partitioner(httpContext);
        }
    }
}

Для создания экземпляра этого класса в его конструктор передается делегат в точности того же типа - с теми же типами параметров и возвращаемого значения - что и в метод PartitionedRateLimiter.Create, который используется для создания глобального ограничителя (о нем я уже писал в этой статье в подразделе “Группировка по одному ключу.” раздела “Настройка глобального ограничителя” ). Так что в этом делегате для выбора и настройки алгоритма ограничения можно использовать те же самые вспомогательные статические методы класса RateLimitPartition для создания и настройки алгоритма ограничения - GetFixedWindowLimiter, GetSlidingWindowLimiter, GetTokenBucketLimiter, GetConcurrencyLimiter - что и для глобального ограничителя. Второй, необязательный, параметр конструктора этого класса - это делегат обратного вызова для обработки отклонения запроса, специфический для этой политики. Он вызывается при получении отказа в выдаче разрешения, я уже писал об этом раньше.

Пример использования безымянной политики

Пример приложения Minimal API, в котором безымянная политика ограничения запросов - с селекцией по пользователю и с использованием алгоритма фиксированного окна - связывается с одной из точек назначения.

using MVVRus.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ => { });
var app = builder.Build();
app.UseRateLimiter();

app.MapGet("/", () => "Hello World!");
app.MapGet("/limited", ()=>"Limited.").RequireRateLimiting(new DelegatedRateLimiterPolicy<String>(
        (HttpContext httpContext) => RateLimitPartition.GetFixedWindowLimiter(
            httpContext.User.Identity?.Name ?? "",
            _ => new FixedWindowRateLimiterOptions { PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }
        )
));

app.Run();

В этом примере запросы с путем “/” (возвращающие строку “Hello World!”) доступны без ограничений, а запросы запросы с путем “/limited” (возвращающие строку “Limited.”) ограничены - их допускается всего десять в минуту. Т.к. аутентификация в этом приложении не настроена (для простоты), то все запросы - анонимные, и потому они попадают под одно и то же ограничение. Если бы в приложении была настроена аутентификация, то запросы от разных пользователей ограничивались бы независимо друг от друга.