Implementar Throttling en nuestra API en .NET Core

Escrito por  Adrian Diaz

Muchas veces cuando implementamos una API Rest nos centramos en du diseño, el control de errores, la nomenclatura de los endpoints, la base de datos que vamos a utilizar, etc... pero muchas veces nos olvidamos de los aspectos de seguridad. En este articulo vamos a ver cómo añadir algunos elementos de seguridad a nuestra API: vamos a ver cómo podemos implementar un middleware de throttling, de una forma muy simple y sencilla.

¿Qué es el throttling?

Antes de empezar con la implementación y el detalle técnico vamos a ver en que consiste el throttling. Podemos definir Throttling como el comportamiento que tiene nuestra aplicación cuando piensa que va a sufrir un ataque o es posible que ponga en peligro el rendimiento de la aplicación. La palabra throttling viene del inglés y significa asfixiamiento o estrangulamiento, pero extrapolado al mundo de la tecnología se refiere al proceso por el cual un chip/servicio rebaja su rendimiento al alcanzar, generalmente, un proceso crítico.

El Throttling es una técnica que se usa hace muchos años refiriéndonos principalmente al hardware (aplicado a la CPU y GPU) pero a también se aplica al desarrollo de software. A los lectores que han desarrollado algún servicio vez contra la API de SharePoint o contra Microsoft Graph seguro que les suena el error 429, según este código lo que indica es que estamos realizando muchas peticiones. Ahora bien, ¿porque se produce este error? En los inicios cuando la API de SharePoint en sus versiones OnPrem el throttling se producía en el momento en el que el servidor tenía una alta carga de CPU y por solucionarlo iba liberando recursos para que el servidor mejorase su rendimiento y pudiera atender otros procesos prioritarios. Posteriormente con la versión Online de SharePoint este 429 se seguía produciendo sin embargo ahora no debería de ser por problemas del servidor (debido a que ahora el servicio tenía otro tipo de infraestructura). ¿Cuándo se produce principalmente? En el momento en el que ponemos un proceso en una Azure Function y esta realiza varias consultas consecutivas a la API, hablamos de que ponemos una Function y esta puede escalar y es posible que desde la misma IP podamos realizar más de 100 requests por minuto, en este momento SharePoint nos vuelve a mostrar este 429. Este 429 nos lo devuelve para evitar un ataque de denegación de servicio contra SharePoint Online, porque lo hacen tenemos una API y si tenemos alguna IP que está realizando multitud de peticiones lo normal desde el punto de vista de la API es limitar este número para evitar que un cliente/IP consuma todos los recursos y eso pueda provocar que nuestro servicio deje de funcionar. Este tipo de servicios vienen incluidos en herramientas que ponemos encima de nuestra API, como puede ser un Azure API Managment. Sin embargo, ¿cómo podemos incluirlo en nuestro desarrollo de .NET Core de una forma fácil?

Manos a la obra

Antes de ponernos a la implementación hay que tener claro cómo queremos implementar el Throttling, es decir, tenemos que conocer nuestra aplicación para saber dónde va a ser efectivo y como lo queremos implementar. Podemos implementar un throttling general que es que cualquier petición que nos realicen la almacenemos y cuando lleguemos a un número de peticiones que consideremos la bloqueamos y listo. Pero está claro que eso es matar moscas a cañonazos, por loque igual debemos de conocer cuáles son los puntos que más nos atacan y en dichos puntos activamos el throttling y de esta forma aplicamos la seguridad en los puntos donde queremos. Para este ejemplo vamos a optar por esta segunda opción ya que creemos que es un caso más parecido a la realidad.

Crearemos el proyecto con la línea de comando de dotnet

Dotnet new webapi

Ahora nos crearemos una carpeta "Infraestructure" donde crearemos la siguiente estructura de carpetas:

  • Dependency Injection Donde pondremos los elementos que vamos a necesitar inyectar en el arranque de nuestra aplicación.

  • Model Crearemos las clases necesarias que vamos a utilizar.

  • Stores En esta carpeta crearemos los distintos tipos de almacenamiento que vamos a utilizar en nuestro desarrollo.

  • Services Aquí implementaremos el servicio de Throttling.

  • Utilities En esta carpeta pondremos todos los métodos, funciones, extensores que hemos implementado para facilitarnos la vida.

Nota: En el ejemplo vamos a mostrar las partes del código más representativas, el resto del código lo puedes consultar y utilizar en el siguiente repositorio.

Elección del almacenamiento

Uno de los puntos fuertes a la hora de añadir una funcionalidad a la API es ver cuál es el tipo de almacenamiento que vamos a utilizar. En este caso vamos a analizar qué es lo que necesitamos:

  • Tener algún sitio donde poder dar alta de forma dinámicamente las reglas que vamos a utilizar para bloquear las peticiones

  • Tener algún lugar donde poder tener almacenada la cuenta que hacemos de cada Request.

Ahora bien, ¿qué características debe tener el tipo de almacenamiento? Lo primero es que las reglas que vamos a aplicar deben de tener un almacenamiento persistente. Debe de ser rápido ya que vamos a realizar peticiones constantes por lo que un Redis puede ser una buena opción. Pero tiene un inconveniente: ¿cómo podemos mantener los datos actualizados en el Redis? Quizás mediante un proceso en segundo plano usando por ejemplo una Azure Function, ¿no? Por otro lado, para el tema del alojamiento de la cuenta de cada Request, necesitamos una información volátil por regla general no nos hace fala que esta información persista más que una hora a los sumo dos y no se debe de persistir en ningún otro sitio. ¿Quizás con una base de datos InMemory puede ser también otra alternativa? Pero que ocurre si tenemos más de un Frontal Web y un balanceador de carga, ¿sería una opción válida? La respuesta es que NO, ... al final tenemos una elección del tipo de almacenamiento dependiendo de nuestras necesidades. El tema de ver que distintos tipos de almacenamiento da para más de un artículo (y no es el objetivo de dicho artículo) y si sentís curiosidad podéis ver la sesión que impartí junto con Robert Bermejo en el pasado Global Azure.

Para este desarrollo vamos a simplificar, vamos a almacenar todo en memoria y dejaremos a la inquietud del lector que implemente otra alternativa. Para ello dentro la carpeta Store nos crearemos las siguientes clases:

ICounterStore.cs

1using Compartimoss.Example.Throttling.Model;
2using System;
3using System.Threading;
4using System.Threading.Tasks;
5namespace Compartimoss.Example.AspNetCore.Throttling.Stores
6{
7    public interface ICounterStore
8    {
9        Task<bool> ExistsAsync(string id, CancellationToken cancellationToken = default);
10        Task<Counter> GetAsync(string id, CancellationToken cancellationToken = default);
11        Task RemoveAsync(string id, CancellationToken cancellationToken = default);
12        Task SetAsync(string id, Counter entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default);
13    }
14}
15

IRuleStore.cs

1using Compartimoss.Example.Throttling.Model;
2using System.Threading;
3using System.Threading.Tasks;
4namespace  Compartimoss.Example.AspNetCore.Throttling.Stores
5{
6    public interface IRuleStore
7    {
8        Task<RuleCollection> GetAsync(CancellationToken cancellationToken = default);
9    }
10}
11

Ahora vamos a su implementación la Store de las Reglas, que directamente se las vamos a pasar dentro de la configuración de la propia WebApi:

1using Compartimoss.Example.Throotling.Model;
2using Microsoft.Extensions.Options;
3using System.Threading;
4using System.Threading.Tasks;
5namespace Compartimoss.Example.AspNetCore.Throttling.Stores
6{
7    internal class ConfigurationRuleStore : IRuleStore
8    {
9        private readonly RuleCollection _rules;
10        public ConfigurationRuleStore(IOptions<RuleCollection> options)
11        {
12            _rules = options.Value;
13        }
14        public Task<RuleCollection> GetAsync(CancellationToken cancellationToken = default)
15        {
16            return Task.FromResult(_rules);
17        }   
18   }
19}
20

Para la implementación de los Contadores, vamos a utilizar la MemoryCache

MemoryCacheCounter.cs

1using Compartimoss.Example.AspNetCore.Throttling.Stores;
2using Compartimoss.Example.Throotling.Model;
3using Microsoft.Extensions.Caching.Memory;
4using System;
5using System.Threading;
6using System.Threading.Tasks;
7namespace Lidl.Library.AspNetCore.Throttling.Stores
8{
9    internal class MemoryCacheCounterStore : ICounterStore
10    {
11        private readonly IMemoryCache _cache;
12        public MemoryCacheCounterStore(IMemoryCache cache)
13        {
14            _cache = cache;
15        }
16        public Task<bool> ExistsAsync(string id, CancellationToken cancellationToken = default)
17        {
18            return Task.FromResult(_cache.TryGetValue(id, out _));
19        }
20        public Task<Counter> GetAsync(string id, CancellationToken cancellationToken = default)
21        {
22            if (_cache.TryGetValue(id, out Counter stored))
23            {
24                return Task.FromResult(stored);
25            }
26            return Task.FromResult(default(Counter));
27        }
28        public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
29        {
30            _cache.Remove(id);
31            return Task.CompletedTask;
32        }
33        public Task SetAsync(string id, Counter entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default)
34        {
35            var options = new MemoryCacheEntryOptions
36            {
37                Priority = CacheItemPriority.NeverRemove
38            };
39            if (expirationTime.HasValue)
40            {
41                options.SetAbsoluteExpiration(expirationTime.Value);
42            }
43            _cache.Set(id, entry, options);
44            return Task.CompletedTask;
45        }
46    }
47}
48

Servicio Throttling

Antes de ponernos manos a la obra con la implementación del middleware vamos a implementar un servicio que cumpla con las necesidades que queremos. Vamos a implementar un servicio que tenga cuatro métodos:

  • Ver si la Request coincide con alguna de las reglas que hemos definido.

  • Ver si cumple los límites establecidos.

  • Comprobar si la dirección que nos llama tiene límites o no.

  • Procesar la Request.

Para ellos nos quedaría un servicio de la siguiente forma:

1using System.Collections. Generic;
2using System.Threading;
3using System.Threading.Tasks;
4using Compartimoss.Example.Throotling.Model;
5using Compartimoss.Example.Throttling.Model;
6namespace Compartimoss.Example.Throttling.Services
7{
8    public interface IThrottlingService
9    {
10        Task<IEnumerable<Rule>> GetMatchingRulesAsync(RequestIdentity identity, CancellationToken cancellationToken = default);
11        RateLimitHeaders GetRateLimitHeaders(Counter counter, Rule rule, CancellationToken cancellationToken = default);
12        Task<Counter> ProcessRequestAsync(RequestIdentity requestIdentity, Rule rule, CancellationToken cancellationToken = default);
13        Task<bool> IsWhitelistedAsync(RequestIdentity requestIdentity, CancellationToken cancellationToken = default);
14    }
15}
16

Su implementación queda de la siguiente forma:

1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Security.Cryptography;
6using System.Text;
7using System.Threading;
8using System.Threading.Tasks;
9using Compartimoss.Example.AspNetCore.Throttling.Stores;
10using Compartimoss.Example.Throotling.Model;
11using Compartimoss.Example.Throttling.Model;
12using Compartimoss.Example.Throttling.Utilities;
13namespace Compartimoss.Example.Throttling.Services
14{
15    public class ThrottlingService : IThrottlingService
16    {
17        /// The key-lock used for limiting requests.
18        private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
19        private readonly IRuleStore _ruleStore;
20        private readonly ICounterStore _counterStore;
21        private readonly ICounterKeyService _counterKeyBuilder;
22        public ThrottlingService(IRuleStore ruleStore, ICounterStore counterStore, ICounterKeyService counterKeyBuilder)
23        {
24            _ruleStore = ruleStore;
25            _counterStore = counterStore;
26            _counterKeyBuilder = counterKeyBuilder;
27        }
28        public async Task<IEnumerable<Rule>> GetMatchingRulesAsync(RequestIdentity identity, CancellationToken cancellationToken = default)
29        {
30            var rules = await _ruleStore.GetAsync(cancellationToken);
31            if (rules?.Rules?.Any() == true)
32            {
33                return rules.Rules
34                            .Where(item => (item.Ip == "*" || IpParser.ContainsIp(item.Ip, identity.Ip)) && RuleMatches(identity, rules, item))
35                            .GroupBy(l => l.Period)
36                            .Select(l => l.OrderBy(x => x.Limit))
37                            .Select(l => l.First())
38                            .OrderBy(l => l.PeriodTimespan)
39                            .ToList();
40            }
41            return new Rule[0];
42        }
43
44        public async Task<bool> IsWhitelistedAsync(RequestIdentity requestIdentity, CancellationToken cancellationToken = default)
45        {
46            var rules = await _ruleStore.GetAsync(cancellationToken);
47            if (rules==null)
48            {
49                return false;
50            }
51            if (rules.IpWhitelist != null && IpParser.ContainsIp(rules.IpWhitelist, requestIdentity.Ip))
52            {
53                return true;
54            }
55            if (rules.EndpointWhitelist != null && rules.EndpointWhitelist.Any())
56            {
57                var path = rules.EnableRegexRuleMatching ? $".+:{requestIdentity.Path}" : $"*:{requestIdentity.Path}";
58                return rules.EndpointWhitelist.Any(x => $"{requestIdentity.HttpMethod}:{requestIdentity.Path}".IsUrlMatch(x, rules.EnableRegexRuleMatching))
59                       || rules.EndpointWhitelist.Any(x => path.IsUrlMatch(x, rules.EnableRegexRuleMatching));
60            }
61            return false;
62        }
63
64        public async Task<Counter> ProcessRequestAsync(RequestIdentity requestIdentity, Rule rule, CancellationToken cancellationToken = default)
65        {
66            var counter = new Counter
67            {
68                Timestamp = DateTime.UtcNow,
69                Count = 1
70            };
71            var counterId = BuildCounterKey(requestIdentity, rule);
72            using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false))
73            {
74                var entry = await _counterStore.GetAsync(counterId, cancellationToken);
75                if (entry != null)
76                {
77                    // entry has not expired
78                    if (entry.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
79                    {
80                        // increment request count
81                        var totalCount = entry.Count + 1;
82                        // deep copy
83                        counter = new Counter
84                        {
85                            Timestamp = entry.Timestamp,
86                            Count = totalCount
87                        };
88                    }
89                }
90                // stores: id (string) - timestamp (datetime) - total_requests (long)
91                await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken);
92            }
93            return counter;
94        }
95
96        public RateLimitHeaders GetRateLimitHeaders(Counter counter, Rule rule, CancellationToken cancellationToken = default)
97        {
98            var reset = (counter?.Timestamp ?? DateTime.UtcNow) + (rule.PeriodTimespan ?? rule.Period.ToTimeSpan());
99            var remaining = rule.Limit - (counter?.Count ?? 0);
100            return new RateLimitHeaders
101            {
102                Reset = reset.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo),
103                Limit = rule.Period,
104                Remaining = remaining.ToString()
105            };
106        }
107
108        private string BuildCounterKey(RequestIdentity requestIdentity, Rule rule)
109        {
110            var key = _counterKeyBuilder.Create(requestIdentity, rule);
111            var bytes = Encoding.UTF8.GetBytes(key);
112            using (var algorithm = new SHA1Managed())
113            {
114                var hash = algorithm.ComputeHash(bytes);
115                return Convert.ToBase64String(hash);
116            }
117        }
118
119        private bool RuleMatches(RequestIdentity identity, RuleCollection rules, Rule item)
120        {
121            var path1 = rules.EnableRegexRuleMatching ? $".+:{identity.Path}" : $"*:{identity.Path}";
122            var path2 = $"{identity.HttpMethod}:{identity.Path}";
123            if (path1.IsUrlMatch(item.Endpoint, rules.EnableRegexRuleMatching))
124            {
125                // search for rules with endpoints like "*" and "*:/matching_path"
126                return true;
127            }
128            if (path2.IsUrlMatch(item.Endpoint, rules.EnableRegexRuleMatching))
129            {
130                // search for rules with endpoints like "matching_verb:/matching_path"
131                return true;
132            }
133            if (item.Endpoint == "*")
134            {
135                return true;
136            }
137            return false;
138        }
139    }
140}
141

Implementación del Middleware

Una vez, ya tenemos todo listo vamos a implementar el middleware para ello debemos de tener claro que solamente debemos de bloquear aquellas peticiones que hayamos definido. Un middleware debemos de tener claro que se tiene que ejecutar muy rápido y que una mala implementación de este puede provocar que nuestro desarrollo tenga un problema de performance. El middleware quedaría de la siguiente forma:

1using Compartimoss.Example.Throotling.Model;
2using Compartimoss.Example.Throttling.Model;
3using Compartimoss.Example.Throttling.Services;
4using Compartimoss.Example.Throttling.Utilities;
5using Microsoft.AspNetCore.Http;
6using Microsoft.Extensions.Logging;
7using Microsoft.Extensions.Options;
8using System;
9using System.Collections.Generic;
10using System.Linq;
11using System.Threading.Tasks;
12namespace  Compartimoss.Example.Throttling
13{
14    internal class ThrottlingMiddleware : IMiddleware
15    {
16        private readonly ThrottlingOptions _options;
17        private readonly IThrottlingService _processor;
18        private readonly IRequestIdentityService _requestIdentityService;
19        private readonly ILogger<ThrottlingMiddleware> _logger;
20        public ThrottlingMiddleware(IOptions<ThrottlingOptions> options, IThrottlingService processor, IRequestIdentityService requestIdentityService, ILogger<ThrottlingMiddleware> logger)
21        {
22            _options = options.Value;
23            _processor = processor;
24            _requestIdentityService = requestIdentityService;
25            _logger = logger;
26        }
27
28        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
29        {
30            // check if rate limiting is enabled
31            if (_options == null)
32            {
33                await next.Invoke(context);
34                return;
35            }
36            // check if throotling is enabled
37            if (!_options.Enabled)
38            {
39                await next.Invoke(context);
40                return;
41            }
42            // compute identity from request
43            var identity = _requestIdentityService.Resolve();
44            // check white list
45            if (await _processor.IsWhitelistedAsync(identity))
46            {
47                await next.Invoke(context);
48                return;
49            }
50            var rules = await _processor.GetMatchingRulesAsync(identity, context.RequestAborted);
51            var rulesDict = new Dictionary<Rule, Counter>();
52            foreach (var rule in rules)
53            {
54                // increment counter
55                var rateLimitCounter = await _processor.ProcessRequestAsync(identity, rule, context.RequestAborted);
56                if (rule.Limit > 0)
57                {
58                    // check if key expired
59                    if (rateLimitCounter.Timestamp + rule.PeriodTimespan.Value < DateTime.UtcNow)
60                    {
61                        continue;
62                    }
63                    // check if limit is reached
64                    if (rateLimitCounter.Count > rule.Limit)
65                    {
66                        //compute retry after value
67                        var retryAfter = rateLimitCounter.Timestamp.RetryAfterFrom(rule);
68                        // log blocked request
69                        LogBlockedRequest(context, identity, rateLimitCounter, rule, _options.Mode);
70                        if (_options.Mode == ThrottlingMode.Restricted)
71                        {
72                            if (_options.RequestBlockedBehaviorAsync != null)
73                            {
74                                await _options.RequestBlockedBehaviorAsync(context, identity, rateLimitCounter, rule);
75                            }
76                            // break execution
77                            await ReturnQuotaExceededResponse(context, rule, retryAfter);
78                            return;
79                        }
80                    }
81                }
82                // if limit is zero or less, block the request.
83                else
84                {
85                    // log blocked request
86                    LogBlockedRequest(context, identity, rateLimitCounter, rule, _options.Mode);
87                    if (_options.Mode == ThrottlingMode.Restricted)
88                    {
89                        if (_options.RequestBlockedBehaviorAsync != null)
90                        {
91                            await _options.RequestBlockedBehaviorAsync(context, identity, rateLimitCounter, rule);
92                        }
93                        // break execution (Int32 max used to represent infinity)
94                        await ReturnQuotaExceededResponse(context, rule, int.MaxValue.ToString(System.Globalization.CultureInfo.InvariantCulture));
95                        return;
96                    }
97                }
98                rulesDict.Add(rule, rateLimitCounter);
99            }
100            // set X-Rate-Limit headers for the longest period
101            if (rulesDict.Any() && !_options.DisableRateLimitHeaders)
102            {
103                var rule = rulesDict.OrderByDescending(x => x.Key.PeriodTimespan).FirstOrDefault();
104                var headers = _processor.GetRateLimitHeaders(rule.Value, rule.Key, context.RequestAborted);
105                headers.Context = context;
106                context.Response.OnStarting(SetRateLimitHeaders, state: headers);
107            }
108            await next.Invoke(context);
109        }
110
111        private Task ReturnQuotaExceededResponse(HttpContext httpContext, Rule rule, string retryAfter)
112        {
113            var message = string.Format(
114                _options.QuotaExceededResponse?.Content ??
115                _options.QuotaExceededMessage ??
116                "API calls quota exceeded! maximum admitted {0} per {1}.", rule.Limit, rule.Period, retryAfter);
117            if (!_options.DisableRateLimitHeaders)
118            {
119                httpContext.Response.Headers["Retry-After"] = retryAfter;
120            }
121            httpContext.Response.StatusCode = _options.QuotaExceededResponse?.StatusCode ?? _options.HttpStatusCode;
122            httpContext.Response.ContentType = _options.QuotaExceededResponse?.ContentType ?? "text/plain";
123            return httpContext.Response.WriteAsync(message);
124        }
125
126        private Task SetRateLimitHeaders(object rateLimitHeaders)
127        {
128            var headers = (RateLimitHeaders)rateLimitHeaders;
129            headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit;
130            headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining;
131            headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset;
132            return Task.CompletedTask;
133        }
134
135        private void LogBlockedRequest(HttpContext httpContext, RequestIdentity identity, Counter counter, Rule rule, ThrottlingMode mode)
136        {
137            var message = (mode == ThrottlingMode.Restricted)
138                ? $"Request {identity.HttpMethod}:{identity.Path} from IP {identity.Ip} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.Count - rule.Limit}. Blocked by rule {rule.Endpoint}, TraceIdentifier {httpContext.TraceIdentifier}."
139                : $"[Skip] Request {identity.HttpMethod}:{identity.Path} from IP {identity.Ip} shold be blocked but i'm in observe mode, quota {rule.Limit}/{rule.Period} exceeded by {counter.Count - rule.Limit}. Blocked by rule {rule.Endpoint}, TraceIdentifier {httpContext.TraceIdentifier}.";
140            _logger.LogWarning(message);
141        }
142    }
143}
144

Configurando el Middleware

Antes de lanzar y probar el funcionamiento de nuestro desarrollo debemos de configurar el arranque de nuestra app según los métodos que hemos implementado en la carpeta de inyección de dependencias. Para ello en el método ConfigureService del Startup añadiremos esta configuración:

1             services.AddThrottling(p =>
2            {
3                p.Enabled = true;
4                p.Mode = ThrottlingMode.Restricted;
5                p.CounterStoreType = CounterStoreType.InMemory;
6            })
7            .AddRules(p => Configuration.GetSection("Throttling").Bind(p));
8

Y en el método Configure algo tan sencillo como:

1  app.UseThrottling();
2

Ahora lo que nos queda es configurar las settings de nuestra aplicación por ejemplo como las siguientes:

1{
2  "Logging": {
3    "LogLevel": {
4      "Default": "Information",
5      "Microsoft": "Warning",
6      "Microsoft.Hosting.Lifetime": "Information"
7    }
8  },
9  "Throttling": {
10    "EndpointWhitelist": [ "*:/WeatherForecast/Three" ],
11    "IpWhitelist": [],
12    "StackBlockedRequests": true,
13    "EnableRegexRuleMatching": false,
14    "Rules": [
15      {
16        "Ip": "*",
17        "Endpoint": "get:/WeatherForecast/*",
18        "Period": "1m",
19        "Limit": 2
20      },
21      {
22        "Ip": "*",
23        "Endpoint": "*:/WeatherForecast",
24        "Period": "2m",
25        "Limit": 3
26      }
27    ]
28  }
29}
30

Si ahora ejecutamos la API: dotnet run y lanzamos hacemos dos peticiones y veremos como la API nos devuelve un 429.

image1

Conclusión

En multitud de ocasiones delegamos responsabilidad de nuestra aplicación en otros servicios, sin embargo, es conveniente saber que al final nuestra aplicación debe de estar siempre activa y si nuestra aplicación se cae la responsabilidad es solamente nuestra. Además de por el tema de responsabilidad también hay un tema de costes que siempre hay que tener en cuenta a la hora de llevar a cabo una implementación. En este caso de una forma sencilla y muy simple podemos evitar que un determinado cliente pueda provocar que nuestro desarrollo se caiga.

También hay multitud paquetes de Throttling en Nuget que podemos utilizar en nuestros desarrollos. https://nugetmusthaves.com/Tag/throttling Sin embargo a la hora de incluirlo en nuestros desarrollos siempre hay que tener en cuenta: la funcionalidad, la calidad del código y el número de usuarios que tiene ese proyecto. Por eso para funcionalidad pequeña y que no cueste mucho esfuerzo yo recomiendo la implementación de dicha característica para no tener una dependencia con dicha librería y su posterior actualización.

Adrián Diaz Cervera
Technical Lead at SCRM Lidl Hub International
MVP Office Development
http://theavenger.dev
@AdrianDiaz81

Siguemos en LinkedInSiguemos en Twitter
Powered by  ENCAMINA