< 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: 157
Uncovered lines: 5
Coverable lines: 162
Total lines: 267
Line coverage: 96.9%
Branch coverage
75%
Covered branches: 24
Total branches: 32
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%11100%
GetClientPartitionKey(...)83.33%1212100%
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    {
 16827        services.Configure<ApplicationRateLimitingOptions>(options =>
 16828        {
 28029            ApplicationRateLimitingOptions defaultOptions = CreateDefaultOptions(environment);
 16830
 28031            options.Enabled = defaultOptions.Enabled;
 28032            options.UseGlobalLimiter = defaultOptions.UseGlobalLimiter;
 28033            options.UseSharedUnknownClientPartition = defaultOptions.UseSharedUnknownClientPartition;
 28034            options.UnknownClientPartitionKey = defaultOptions.UnknownClientPartitionKey;
 16835
 28036            options.GlobalFixedWindow.PermitLimit = defaultOptions.GlobalFixedWindow.PermitLimit;
 28037            options.GlobalFixedWindow.WindowSeconds = defaultOptions.GlobalFixedWindow.WindowSeconds;
 28038            options.GlobalFixedWindow.QueueLimit = defaultOptions.GlobalFixedWindow.QueueLimit;
 16839
 28040            options.FixedWindowPolicy.PermitLimit = defaultOptions.FixedWindowPolicy.PermitLimit;
 28041            options.FixedWindowPolicy.WindowSeconds = defaultOptions.FixedWindowPolicy.WindowSeconds;
 28042            options.FixedWindowPolicy.QueueLimit = defaultOptions.FixedWindowPolicy.QueueLimit;
 16843
 28044            options.ConcurrencyPolicy.PermitLimit = defaultOptions.ConcurrencyPolicy.PermitLimit;
 28045            options.ConcurrencyPolicy.QueueLimit = defaultOptions.ConcurrencyPolicy.QueueLimit;
 44846        });
 47
 16848        services
 16849            .AddOptions<ApplicationRateLimitingOptions>()
 16850            .Bind(configuration.GetSection(ApplicationRateLimitingOptions.SectionName))
 28051            .Validate(options => !string.IsNullOrWhiteSpace(options.UnknownClientPartitionKey),
 16852                "ProjectTemplate:RateLimiting:UnknownClientPartitionKey must not be empty.")
 28053            .Validate(options => options.GlobalFixedWindow.PermitLimit > 0,
 16854                "ProjectTemplate:RateLimiting:GlobalFixedWindow:PermitLimit must be greater than zero.")
 28055            .Validate(options => options.GlobalFixedWindow.WindowSeconds > 0,
 16856                "ProjectTemplate:RateLimiting:GlobalFixedWindow:WindowSeconds must be greater than zero.")
 28057            .Validate(options => options.GlobalFixedWindow.QueueLimit >= 0,
 16858                "ProjectTemplate:RateLimiting:GlobalFixedWindow:QueueLimit must be zero or greater.")
 28059            .Validate(options => options.FixedWindowPolicy.PermitLimit > 0,
 16860                "ProjectTemplate:RateLimiting:FixedWindowPolicy:PermitLimit must be greater than zero.")
 28061            .Validate(options => options.FixedWindowPolicy.WindowSeconds > 0,
 16862                "ProjectTemplate:RateLimiting:FixedWindowPolicy:WindowSeconds must be greater than zero.")
 28063            .Validate(options => options.FixedWindowPolicy.QueueLimit >= 0,
 16864                "ProjectTemplate:RateLimiting:FixedWindowPolicy:QueueLimit must be zero or greater.")
 28065            .Validate(options => options.ConcurrencyPolicy.PermitLimit > 0,
 16866                "ProjectTemplate:RateLimiting:ConcurrencyPolicy:PermitLimit must be greater than zero.")
 28067            .Validate(options => options.ConcurrencyPolicy.QueueLimit >= 0,
 16868                "ProjectTemplate:RateLimiting:ConcurrencyPolicy:QueueLimit must be zero or greater.")
 16869            .ValidateOnStart();
 70
 16871        _ = services.AddRateLimiter();
 72
 16873        _ = services.AddOptions<RateLimiterOptions>()
 16874            .Configure<IOptions<ApplicationRateLimitingOptions>>((options, rateLimitingOptionsAccessor) =>
 16875            {
 13676                ApplicationRateLimitingOptions rateLimitingOptions = rateLimitingOptionsAccessor.Value;
 16877
 13678                options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
 16879
 13680                options.OnRejected = async (context, cancellationToken) =>
 13681                {
 882                    HttpContext httpContext = context.HttpContext;
 883                    HttpResponse response = httpContext.Response;
 13684
 885                    TimeSpan? retryAfter = null;
 13686
 887                    if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan retryAfterValue))
 13688                    {
 689                        retryAfter = retryAfterValue;
 13690
 691                        response.Headers.RetryAfter =
 692                            Math.Ceiling(retryAfterValue.TotalSeconds)
 693                                .ToString(CultureInfo.InvariantCulture);
 13694                    }
 13695
 896                    ILogger logger = httpContext.RequestServices
 897                        .GetRequiredService<ILoggerFactory>()
 898                        .CreateLogger("Template.Web.RateLimiting");
 13699
 8100                    LogRateLimitRejectedRequest(
 8101                        logger,
 8102                        httpContext.Request.Method,
 8103                        httpContext.Request.Path.Value ?? string.Empty,
 8104                        httpContext.Connection.RemoteIpAddress?.ToString(),
 8105                        httpContext.GetEndpoint()?.DisplayName,
 8106                        retryAfter?.TotalSeconds,
 8107                        httpContext.TraceIdentifier);
 136108
 8109                    response.StatusCode = StatusCodes.Status429TooManyRequests;
 8110                    response.ContentType = "application/json";
 136111
 8112                    await response.WriteAsJsonAsync(new
 8113                    {
 8114                        error = "Too many requests.",
 8115                        statusCode = StatusCodes.Status429TooManyRequests,
 8116                        traceId = httpContext.TraceIdentifier
 8117                    }, cancellationToken);
 144118                };
 168119
 136120                if (!rateLimitingOptions.Enabled)
 168121                {
 2122                    return;
 168123                }
 168124
 134125                if (rateLimitingOptions.UseGlobalLimiter)
 168126                {
 130127                    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
 252128                        RateLimitPartition.GetFixedWindowLimiter(
 252129                            partitionKey: GetClientPartitionKey(httpContext, rateLimitingOptions),
 346130                            factory: _ => CreateFixedWindowRateLimiterOptions(rateLimitingOptions.GlobalFixedWindow)));
 168131                }
 168132
 134133                options.AddPolicy(ApplicationRateLimitingPolicyNames.Fixed, httpContext =>
 140134                    RateLimitPartition.GetFixedWindowLimiter(
 140135                        partitionKey: GetClientPartitionKey(httpContext, rateLimitingOptions),
 142136                        factory: _ => CreateFixedWindowRateLimiterOptions(rateLimitingOptions.FixedWindowPolicy)));
 168137
 134138                options.AddPolicy(ApplicationRateLimitingPolicyNames.Concurrency, httpContext =>
 140139                    RateLimitPartition.GetConcurrencyLimiter(
 140140                        partitionKey: GetEndpointPartitionKey(httpContext),
 142141                        factory: _ => CreateConcurrencyLimiterOptions(rateLimitingOptions.ConcurrencyPolicy)));
 302142            });
 143
 168144        return services;
 145    }
 146    private static ApplicationRateLimitingOptions CreateDefaultOptions(IHostEnvironment environment)
 147    {
 280148        ApplicationRateLimitingOptions options = new();
 149
 280150        if (environment.IsDevelopment())
 151        {
 0152            options.GlobalFixedWindow.PermitLimit = 300;
 0153            options.GlobalFixedWindow.WindowSeconds = 60;
 154
 0155            options.FixedWindowPolicy.PermitLimit = 120;
 0156            options.FixedWindowPolicy.WindowSeconds = 60;
 157
 0158            options.ConcurrencyPolicy.PermitLimit = 20;
 159        }
 160
 280161        return options;
 162    }
 163
 164    private static FixedWindowRateLimiterOptions CreateFixedWindowRateLimiterOptions(
 165        FixedWindowRateLimitingOptions options)
 166    {
 96167        return new FixedWindowRateLimiterOptions
 96168        {
 96169            AutoReplenishment = true,
 96170            PermitLimit = EnsureAtLeast(options.PermitLimit, minimum: 1),
 96171            Window = TimeSpan.FromSeconds(EnsureAtLeast(options.WindowSeconds, minimum: 1)),
 96172            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
 96173            QueueLimit = EnsureAtLeast(options.QueueLimit, minimum: 0)
 96174        };
 175    }
 176
 177    private static ConcurrencyLimiterOptions CreateConcurrencyLimiterOptions(
 178        ConcurrencyRateLimitingOptions options)
 179    {
 2180        return new ConcurrencyLimiterOptions
 2181        {
 2182            PermitLimit = EnsureAtLeast(options.PermitLimit, minimum: 1),
 2183            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
 2184            QueueLimit = EnsureAtLeast(options.QueueLimit, minimum: 0)
 2185        };
 186    }
 187
 188    private static string GetClientPartitionKey(
 189        HttpContext httpContext,
 190        ApplicationRateLimitingOptions options)
 191    {
 128192        ILogger logger = httpContext.RequestServices
 128193            .GetRequiredService<ILoggerFactory>()
 128194            .CreateLogger("Template.Web.RateLimiting");
 195
 128196        return GetClientPartitionKey(httpContext, options, logger);
 197    }
 198
 199    internal static string GetClientPartitionKey(
 200        HttpContext httpContext,
 201        ApplicationRateLimitingOptions options,
 202        ILogger logger)
 203    {
 134204        string? remoteIpAddress = httpContext.Connection.RemoteIpAddress?.ToString();
 205
 134206        if (!string.IsNullOrWhiteSpace(remoteIpAddress))
 207        {
 4208            return remoteIpAddress;
 209        }
 210
 130211        string fallbackPartitionKey = string.IsNullOrWhiteSpace(options.UnknownClientPartitionKey)
 130212            ? "unknown-client"
 130213            : options.UnknownClientPartitionKey.Trim();
 130214        string fallbackMode = options.UseSharedUnknownClientPartition ? "Shared" : "PerRequest";
 130215        string fallbackDiscriminator = string.IsNullOrWhiteSpace(httpContext.TraceIdentifier)
 130216            ? Guid.NewGuid().ToString("N")
 130217            : httpContext.TraceIdentifier;
 218
 130219        if (!options.UseSharedUnknownClientPartition)
 220        {
 110221            fallbackPartitionKey = $"{fallbackPartitionKey}:{fallbackDiscriminator}";
 222        }
 223
 130224        LogRateLimitingClientPartitionFallback(
 130225            logger,
 130226            fallbackMode,
 130227            fallbackPartitionKey,
 130228            fallbackDiscriminator);
 229
 130230        return fallbackPartitionKey;
 231    }
 232
 233    private static string GetEndpointPartitionKey(HttpContext httpContext)
 234    {
 6235        return httpContext.GetEndpoint()?.DisplayName
 6236            ?? httpContext.Request.Path.Value
 6237            ?? "unknown-endpoint";
 238    }
 239
 240    private static int EnsureAtLeast(int value, int minimum)
 241    {
 292242        return value < minimum ? minimum : value;
 243    }
 244
 245    [LoggerMessage(
 246        EventId = 6001,
 247        Level = LogLevel.Warning,
 248        Message = "Rate limit rejected request. Method: {Method}; Path: {Path}; RemoteIpAddress: {RemoteIpAddress}; Endp
 249    private static partial void LogRateLimitRejectedRequest(
 250        ILogger logger,
 251        string method,
 252        string path,
 253        string? remoteIpAddress,
 254        string? endpoint,
 255        double? retryAfterSeconds,
 256        string traceIdentifier);
 257
 258    [LoggerMessage(
 259        EventId = 6002,
 260        Level = LogLevel.Warning,
 261        Message = "Rate limiting used fallback client partition because RemoteIpAddress was unavailable. FallbackMode: {
 262    private static partial void LogRateLimitingClientPartitionFallback(
 263        ILogger logger,
 264        string fallbackMode,
 265        string partitionKey,
 266        string traceIdentifier);
 267}