< Summary

Information
Class: ProjectTemplate.Web.Extensions.RateLimitingServiceExtensions
Assembly: ProjectTemplate.Web
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/RateLimitingServiceExtensions.cs
Line coverage
96%
Covered lines: 132
Uncovered lines: 5
Coverable lines: 137
Total lines: 213
Line coverage: 96.3%
Branch coverage
75%
Covered branches: 18
Total branches: 24
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddApplicationRateLimiting(...)100%1010100%
CreateDefaultOptions(...)50%3237.5%
CreateFixedWindowRateLimiterOptions(...)100%11100%
CreateConcurrencyLimiterOptions(...)100%11100%
GetClientPartitionKey(...)100%44100%
GetEndpointPartitionKey(...)33.33%66100%
EnsureAtLeast(...)50%22100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/RateLimitingServiceExtensions.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Threading.RateLimiting;
 3using Microsoft.AspNetCore.RateLimiting;
 4using Microsoft.Extensions.Options;
 5using ProjectTemplate.Web.Constants;
 6using ProjectTemplate.Web.Options;
 7
 8namespace ProjectTemplate.Web.Extensions;
 9
 10/// <summary>
 11/// Provides extension methods to register rate limiting services for the application.
 12/// </summary>
 13public static partial class RateLimitingServiceExtensions
 14{
 15    /// <summary>
 16    /// Adds the application's predefined rate limiting policies to the service collection.
 17    /// </summary>
 18    /// <param name="services">The <see cref="IServiceCollection"/> to add the rate limiting services to.</param>
 19    /// <param name="configuration">The application configuration source.</param>
 20    /// <param name="environment">The current hosting environment.</param>
 21    /// <returns>The same <see cref="IServiceCollection"/> instance so calls can be chained.</returns>
 22    public static IServiceCollection AddApplicationRateLimiting(
 23        this IServiceCollection services,
 24        IConfiguration configuration,
 25        IHostEnvironment environment)
 26    {
 15427        services.Configure<ApplicationRateLimitingOptions>(options =>
 15428        {
 26029            ApplicationRateLimitingOptions defaultOptions = CreateDefaultOptions(environment);
 15430
 26031            options.Enabled = defaultOptions.Enabled;
 26032            options.UseGlobalLimiter = defaultOptions.UseGlobalLimiter;
 15433
 26034            options.GlobalFixedWindow.PermitLimit = defaultOptions.GlobalFixedWindow.PermitLimit;
 26035            options.GlobalFixedWindow.WindowSeconds = defaultOptions.GlobalFixedWindow.WindowSeconds;
 26036            options.GlobalFixedWindow.QueueLimit = defaultOptions.GlobalFixedWindow.QueueLimit;
 15437
 26038            options.FixedWindowPolicy.PermitLimit = defaultOptions.FixedWindowPolicy.PermitLimit;
 26039            options.FixedWindowPolicy.WindowSeconds = defaultOptions.FixedWindowPolicy.WindowSeconds;
 26040            options.FixedWindowPolicy.QueueLimit = defaultOptions.FixedWindowPolicy.QueueLimit;
 15441
 26042            options.ConcurrencyPolicy.PermitLimit = defaultOptions.ConcurrencyPolicy.PermitLimit;
 26043            options.ConcurrencyPolicy.QueueLimit = defaultOptions.ConcurrencyPolicy.QueueLimit;
 41444        });
 45
 15446        services
 15447            .AddOptions<ApplicationRateLimitingOptions>()
 15448            .Bind(configuration.GetSection(ApplicationRateLimitingOptions.SectionName))
 26049            .Validate(options => options.GlobalFixedWindow.PermitLimit > 0,
 15450                "ProjectTemplate:RateLimiting:GlobalFixedWindow:PermitLimit must be greater than zero.")
 26051            .Validate(options => options.GlobalFixedWindow.WindowSeconds > 0,
 15452                "ProjectTemplate:RateLimiting:GlobalFixedWindow:WindowSeconds must be greater than zero.")
 26053            .Validate(options => options.GlobalFixedWindow.QueueLimit >= 0,
 15454                "ProjectTemplate:RateLimiting:GlobalFixedWindow:QueueLimit must be zero or greater.")
 26055            .Validate(options => options.FixedWindowPolicy.PermitLimit > 0,
 15456                "ProjectTemplate:RateLimiting:FixedWindowPolicy:PermitLimit must be greater than zero.")
 26057            .Validate(options => options.FixedWindowPolicy.WindowSeconds > 0,
 15458                "ProjectTemplate:RateLimiting:FixedWindowPolicy:WindowSeconds must be greater than zero.")
 26059            .Validate(options => options.FixedWindowPolicy.QueueLimit >= 0,
 15460                "ProjectTemplate:RateLimiting:FixedWindowPolicy:QueueLimit must be zero or greater.")
 26061            .Validate(options => options.ConcurrencyPolicy.PermitLimit > 0,
 15462                "ProjectTemplate:RateLimiting:ConcurrencyPolicy:PermitLimit must be greater than zero.")
 26063            .Validate(options => options.ConcurrencyPolicy.QueueLimit >= 0,
 15464                "ProjectTemplate:RateLimiting:ConcurrencyPolicy:QueueLimit must be zero or greater.")
 15465            .ValidateOnStart();
 66
 15467        _ = services.AddRateLimiter();
 68
 15469        _ = services.AddOptions<RateLimiterOptions>()
 15470            .Configure<IOptions<ApplicationRateLimitingOptions>>((options, rateLimitingOptionsAccessor) =>
 15471            {
 12672                ApplicationRateLimitingOptions rateLimitingOptions = rateLimitingOptionsAccessor.Value;
 15473
 12674                options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
 15475
 12676                options.OnRejected = async (context, cancellationToken) =>
 12677                {
 878                    HttpContext httpContext = context.HttpContext;
 879                    HttpResponse response = httpContext.Response;
 12680
 881                    TimeSpan? retryAfter = null;
 12682
 883                    if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan retryAfterValue))
 12684                    {
 685                        retryAfter = retryAfterValue;
 12686
 687                        response.Headers.RetryAfter =
 688                            Math.Ceiling(retryAfterValue.TotalSeconds)
 689                                .ToString(CultureInfo.InvariantCulture);
 12690                    }
 12691
 892                    ILogger logger = httpContext.RequestServices
 893                        .GetRequiredService<ILoggerFactory>()
 894                        .CreateLogger("Template.Web.RateLimiting");
 12695
 896                    LogRateLimitRejectedRequest(
 897                        logger,
 898                        httpContext.Request.Method,
 899                        httpContext.Request.Path.Value ?? string.Empty,
 8100                        httpContext.Connection.RemoteIpAddress?.ToString(),
 8101                        httpContext.GetEndpoint()?.DisplayName,
 8102                        retryAfter?.TotalSeconds,
 8103                        httpContext.TraceIdentifier);
 126104
 8105                    response.StatusCode = StatusCodes.Status429TooManyRequests;
 8106                    response.ContentType = "application/json";
 126107
 8108                    await response.WriteAsJsonAsync(new
 8109                    {
 8110                        error = "Too many requests.",
 8111                        statusCode = StatusCodes.Status429TooManyRequests,
 8112                        traceId = httpContext.TraceIdentifier
 8113                    }, cancellationToken);
 134114                };
 154115
 126116                if (!rateLimitingOptions.Enabled)
 154117                {
 2118                    return;
 154119                }
 154120
 124121                if (rateLimitingOptions.UseGlobalLimiter)
 154122                {
 120123                    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
 232124                        RateLimitPartition.GetFixedWindowLimiter(
 232125                            partitionKey: GetClientPartitionKey(httpContext),
 318126                            factory: _ => CreateFixedWindowRateLimiterOptions(rateLimitingOptions.GlobalFixedWindow)));
 154127                }
 154128
 124129                options.AddPolicy(ApplicationRateLimitingPolicyNames.Fixed, httpContext =>
 130130                    RateLimitPartition.GetFixedWindowLimiter(
 130131                        partitionKey: GetClientPartitionKey(httpContext),
 132132                        factory: _ => CreateFixedWindowRateLimiterOptions(rateLimitingOptions.FixedWindowPolicy)));
 154133
 124134                options.AddPolicy(ApplicationRateLimitingPolicyNames.Concurrency, httpContext =>
 130135                    RateLimitPartition.GetConcurrencyLimiter(
 130136                        partitionKey: GetEndpointPartitionKey(httpContext),
 132137                        factory: _ => CreateConcurrencyLimiterOptions(rateLimitingOptions.ConcurrencyPolicy)));
 278138            });
 139
 154140        return services;
 141    }
 142    private static ApplicationRateLimitingOptions CreateDefaultOptions(IHostEnvironment environment)
 143    {
 260144        ApplicationRateLimitingOptions options = new();
 145
 260146        if (environment.IsDevelopment())
 147        {
 0148            options.GlobalFixedWindow.PermitLimit = 300;
 0149            options.GlobalFixedWindow.WindowSeconds = 60;
 150
 0151            options.FixedWindowPolicy.PermitLimit = 120;
 0152            options.FixedWindowPolicy.WindowSeconds = 60;
 153
 0154            options.ConcurrencyPolicy.PermitLimit = 20;
 155        }
 156
 260157        return options;
 158    }
 159
 160    private static FixedWindowRateLimiterOptions CreateFixedWindowRateLimiterOptions(
 161        FixedWindowRateLimitingOptions options)
 162    {
 88163        return new FixedWindowRateLimiterOptions
 88164        {
 88165            AutoReplenishment = true,
 88166            PermitLimit = EnsureAtLeast(options.PermitLimit, minimum: 1),
 88167            Window = TimeSpan.FromSeconds(EnsureAtLeast(options.WindowSeconds, minimum: 1)),
 88168            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
 88169            QueueLimit = EnsureAtLeast(options.QueueLimit, minimum: 0)
 88170        };
 171    }
 172
 173    private static ConcurrencyLimiterOptions CreateConcurrencyLimiterOptions(
 174        ConcurrencyRateLimitingOptions options)
 175    {
 2176        return new ConcurrencyLimiterOptions
 2177        {
 2178            PermitLimit = EnsureAtLeast(options.PermitLimit, minimum: 1),
 2179            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
 2180            QueueLimit = EnsureAtLeast(options.QueueLimit, minimum: 0)
 2181        };
 182    }
 183
 184    private static string GetClientPartitionKey(HttpContext httpContext)
 185    {
 118186        return httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown-client";
 187    }
 188
 189    private static string GetEndpointPartitionKey(HttpContext httpContext)
 190    {
 6191        return httpContext.GetEndpoint()?.DisplayName
 6192            ?? httpContext.Request.Path.Value
 6193            ?? "unknown-endpoint";
 194    }
 195
 196    private static int EnsureAtLeast(int value, int minimum)
 197    {
 268198        return value < minimum ? minimum : value;
 199    }
 200
 201    [LoggerMessage(
 202        EventId = 6001,
 203        Level = LogLevel.Warning,
 204        Message = "Rate limit rejected request. Method: {Method}; Path: {Path}; RemoteIpAddress: {RemoteIpAddress}; Endp
 205    private static partial void LogRateLimitRejectedRequest(
 206        ILogger logger,
 207        string method,
 208        string path,
 209        string? remoteIpAddress,
 210        string? endpoint,
 211        double? retryAfterSeconds,
 212        string traceIdentifier);
 213}