Skip to content

Commit ef85056

Browse files
committed
feat: add rate limiting and refactor DI setup with ServiceCollection extensions
1 parent cbe8e2d commit ef85056

File tree

13 files changed

+383
-66
lines changed

13 files changed

+383
-66
lines changed

.codacy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ exclude_paths:
2424
- '**/Configurations/**'
2525
- '**/Data/**'
2626
- '**/Enums/**'
27+
- '**/Extensions/**'
2728
- '**/Mappings/**'
2829
- '**/Migrations/**'
2930
- '**/Models/**'

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ ignore:
5858
- .*\/Configurations\/.*
5959
- .*\/Data\/.*
6060
- .*\/Enums\/.*
61+
- .*\/Extensions\/.*
6162
- .*\/Mappings\/.*
6263
- .*\/Migrations\/.*
6364
- .*\/Models\/.*
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Dotnet.Samples.AspNetCore.WebApi.Configurations;
2+
3+
/// <summary>
4+
/// Configuration options for rate limiting.
5+
/// This class defines the parameters for request limits, time windows, and queuing.
6+
/// It is used to control the rate of incoming requests to the API.
7+
/// The options are typically loaded from the appsettings.json configuration file.
8+
/// </summary>
9+
public class RateLimitingOptions
10+
{
11+
/// <summary>
12+
/// Maximum number of requests allowed per window.
13+
/// </summary>
14+
public int PermitLimit { get; set; } = 10;
15+
16+
/// <summary>
17+
/// Time window duration in seconds.
18+
/// </summary>
19+
public int WindowSeconds { get; set; } = 60;
20+
21+
/// <summary>
22+
/// Maximum number of queued requests.
23+
/// </summary>
24+
public int QueueLimit { get; set; } = 0;
25+
}

src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup Label="Development dependencies">
11+
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
1112
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0">
1213
<PrivateAssets>all</PrivateAssets>
1314
</PackageReference>
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using System.Threading.RateLimiting;
2+
using Dotnet.Samples.AspNetCore.WebApi.Configurations;
3+
using Dotnet.Samples.AspNetCore.WebApi.Data;
4+
using Dotnet.Samples.AspNetCore.WebApi.Mappings;
5+
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
6+
using Dotnet.Samples.AspNetCore.WebApi.Services;
7+
using Dotnet.Samples.AspNetCore.WebApi.Utilities;
8+
using Dotnet.Samples.AspNetCore.WebApi.Validators;
9+
using FluentValidation;
10+
using Microsoft.EntityFrameworkCore;
11+
using Microsoft.OpenApi.Models;
12+
using Serilog;
13+
14+
namespace Dotnet.Samples.AspNetCore.WebApi.Extensions;
15+
16+
/// <summary>
17+
/// Extension methods for WebApplicationBuilder to encapsulate service configuration.
18+
/// </summary>
19+
public static class ServiceCollectionExtensions
20+
{
21+
/// <summary>
22+
/// Adds DbContextPool with SQLite configuration for PlayerDbContext.
23+
/// </summary>
24+
/// <param name="services">The IServiceCollection instance.</param>
25+
/// <param name="environment">The web host environment.</param>
26+
/// <returns>The IServiceCollection for method chaining.</returns>
27+
public static IServiceCollection AddDbContextPoolWithSqlite(
28+
this IServiceCollection services,
29+
IWebHostEnvironment environment
30+
)
31+
{
32+
services.AddDbContextPool<PlayerDbContext>(options =>
33+
{
34+
var dataSource = Path.Combine(
35+
AppContext.BaseDirectory,
36+
"storage",
37+
"players-sqlite3.db"
38+
);
39+
options.UseSqlite($"Data Source={dataSource}");
40+
41+
if (environment.IsDevelopment())
42+
{
43+
options.EnableSensitiveDataLogging();
44+
options.LogTo(Log.Logger.Information, LogLevel.Information);
45+
}
46+
});
47+
48+
return services;
49+
}
50+
51+
/// <summary>
52+
/// Adds a default CORS policy that allows any origin, method, and header.
53+
/// <br />
54+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/security/cors"/>
55+
/// </summary>
56+
/// <param name="services">The IServiceCollection instance.</param>
57+
/// <returns>The IServiceCollection for method chaining.</returns>
58+
public static IServiceCollection AddCorsDefaultPolicy(this IServiceCollection services)
59+
{
60+
services.AddCors(options =>
61+
{
62+
options.AddDefaultPolicy(corsBuilder =>
63+
{
64+
corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
65+
});
66+
});
67+
68+
return services;
69+
}
70+
71+
/// <summary>
72+
/// Adds rate limiting configuration with IP-based partitioning.
73+
/// <br />
74+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit"/>
75+
/// </summary>
76+
/// <param name="services">The IServiceCollection instance.</param>
77+
/// <param name="configuration">The application configuration.</param>
78+
/// <returns>The IServiceCollection for method chaining.</returns>
79+
public static IServiceCollection AddRateLimiting(
80+
this IServiceCollection services,
81+
IConfiguration configuration
82+
)
83+
{
84+
// Configure and register options
85+
services.Configure<RateLimitingOptions>(configuration.GetSection("RateLimiting"));
86+
87+
var rateLimitingOptions =
88+
configuration.GetSection("RateLimiting").Get<RateLimitingOptions>()
89+
?? new RateLimitingOptions();
90+
91+
services.AddRateLimiter(options =>
92+
{
93+
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
94+
httpContext =>
95+
{
96+
var partitionKey = RateLimitingUtilities.CreatePartitionKey(httpContext);
97+
98+
return RateLimitPartition.GetFixedWindowLimiter(
99+
partitionKey: partitionKey,
100+
factory: _ => new FixedWindowRateLimiterOptions
101+
{
102+
PermitLimit = rateLimitingOptions.PermitLimit,
103+
Window = TimeSpan.FromSeconds(rateLimitingOptions.WindowSeconds),
104+
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
105+
QueueLimit = rateLimitingOptions.QueueLimit
106+
}
107+
);
108+
}
109+
);
110+
111+
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
112+
113+
// Enhanced OnRejected handler with essential headers
114+
options.OnRejected = (context, _) =>
115+
{
116+
var response = context.HttpContext.Response;
117+
118+
var windowSeconds = rateLimitingOptions.WindowSeconds;
119+
120+
// Essential headers for rate limiting
121+
response.Headers.RetryAfter = windowSeconds.ToString();
122+
response.Headers.Append(
123+
"X-RateLimit-Limit",
124+
rateLimitingOptions.PermitLimit.ToString()
125+
);
126+
response.Headers.Append("X-RateLimit-Remaining", "0");
127+
response.Headers.Append("X-RateLimit-Window", windowSeconds.ToString());
128+
129+
return ValueTask.CompletedTask;
130+
};
131+
});
132+
133+
return services;
134+
}
135+
136+
/// <summary>
137+
/// Adds FluentValidation validators for Player models.
138+
/// <br />
139+
/// <see href="https://docs.fluentvalidation.net/en/latest/aspnet.html"/>
140+
/// </summary>
141+
/// <param name="services">The IServiceCollection instance.</param>
142+
/// <returns>The IServiceCollection for method chaining.</returns>
143+
public static IServiceCollection AddValidators(this IServiceCollection services)
144+
{
145+
services.AddValidatorsFromAssemblyContaining<PlayerRequestModelValidator>();
146+
return services;
147+
}
148+
149+
/// <summary>
150+
/// Sets up Swagger documentation generation and UI for the API.
151+
/// <br />
152+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle" />
153+
/// </summary>
154+
/// <param name="services">The IServiceCollection instance.</param>
155+
/// <param name="configuration">The application configuration.</param>
156+
/// <returns>The IServiceCollection for method chaining.</returns>
157+
public static IServiceCollection AddSwaggerConfiguration(
158+
this IServiceCollection services,
159+
IConfiguration configuration
160+
)
161+
{
162+
services.AddSwaggerGen(options =>
163+
{
164+
options.SwaggerDoc("v1", configuration.GetSection("OpenApiInfo").Get<OpenApiInfo>());
165+
options.IncludeXmlComments(SwaggerUtilities.ConfigureXmlCommentsFilePath());
166+
options.AddSecurityDefinition("Bearer", SwaggerUtilities.ConfigureSecurityDefinition());
167+
options.OperationFilter<AuthorizeCheckOperationFilter>();
168+
});
169+
170+
return services;
171+
}
172+
173+
/// <summary>
174+
/// Registers the PlayerService with the DI container.
175+
/// <br />
176+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection"/>
177+
/// </summary>
178+
/// <param name="services">The IServiceCollection instance.</param>
179+
/// <returns>The IServiceCollection for method chaining.</returns>
180+
public static IServiceCollection RegisterPlayerService(this IServiceCollection services)
181+
{
182+
services.AddScoped<IPlayerService, PlayerService>();
183+
return services;
184+
}
185+
186+
/// <summary>
187+
/// Adds AutoMapper configuration for Player mappings.
188+
/// <br />
189+
/// <see href="https://docs.automapper.io/en/latest/Dependency-injection.html#asp-net-core"/>
190+
/// </summary>
191+
/// <param name="services">The IServiceCollection instance.</param>
192+
/// <returns>The IServiceCollection for method chaining.</returns>
193+
public static IServiceCollection AddMappings(this IServiceCollection services)
194+
{
195+
services.AddAutoMapper(typeof(PlayerMappingProfile));
196+
return services;
197+
}
198+
199+
/// <summary>
200+
/// Registers the PlayerRepository service with the DI container.
201+
/// <br />
202+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection"/>
203+
/// </summary>
204+
/// <param name="services">The IServiceCollection instance.</param>
205+
/// <returns>The IServiceCollection for method chaining.</returns>
206+
public static IServiceCollection RegisterPlayerRepository(this IServiceCollection services)
207+
{
208+
services.AddScoped<IPlayerRepository, PlayerRepository>();
209+
return services;
210+
}
211+
}
Lines changed: 30 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,59 @@
1-
using Dotnet.Samples.AspNetCore.WebApi.Configurations;
2-
using Dotnet.Samples.AspNetCore.WebApi.Data;
3-
using Dotnet.Samples.AspNetCore.WebApi.Mappings;
4-
using Dotnet.Samples.AspNetCore.WebApi.Models;
5-
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
6-
using Dotnet.Samples.AspNetCore.WebApi.Services;
7-
using Dotnet.Samples.AspNetCore.WebApi.Validators;
8-
using FluentValidation;
9-
using Microsoft.EntityFrameworkCore;
10-
using Microsoft.OpenApi.Models;
1+
using Dotnet.Samples.AspNetCore.WebApi.Extensions;
112
using Serilog;
123

13-
var builder = WebApplication.CreateBuilder(args);
14-
154
/* -----------------------------------------------------------------------------
16-
* Configuration
5+
* Web Application
6+
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup
177
* -------------------------------------------------------------------------- */
8+
9+
var builder = WebApplication.CreateBuilder(args);
10+
11+
/* Configurations ----------------------------------------------------------- */
12+
1813
builder
1914
.Configuration.SetBasePath(AppContext.BaseDirectory)
2015
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
2116
.AddEnvironmentVariables();
2217

23-
/* -----------------------------------------------------------------------------
24-
* Logging
25-
* -------------------------------------------------------------------------- */
26-
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
18+
/* Logging ------------------------------------------------------------------ */
2719

28-
/* Serilog ------------------------------------------------------------------ */
20+
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
2921
builder.Host.UseSerilog();
3022

31-
/* -----------------------------------------------------------------------------
32-
* Services
33-
* -------------------------------------------------------------------------- */
23+
/* Controllers -------------------------------------------------------------- */
24+
3425
builder.Services.AddControllers();
26+
builder.Services.AddCorsDefaultPolicy();
27+
builder.Services.AddRateLimiting(builder.Configuration);
28+
builder.Services.AddHealthChecks();
29+
builder.Services.AddValidators();
3530

36-
/* Entity Framework Core ---------------------------------------------------- */
37-
builder.Services.AddDbContextPool<PlayerDbContext>(options =>
31+
if (builder.Environment.IsDevelopment())
3832
{
39-
var dataSource = Path.Combine(AppContext.BaseDirectory, "storage", "players-sqlite3.db");
40-
41-
options.UseSqlite($"Data Source={dataSource}");
33+
builder.Services.AddSwaggerConfiguration(builder.Configuration);
34+
}
4235

43-
if (builder.Environment.IsDevelopment())
44-
{
45-
options.EnableSensitiveDataLogging();
46-
options.LogTo(Log.Logger.Information, LogLevel.Information);
47-
}
48-
});
36+
/* Services ----------------------------------------------------------------- */
4937

50-
builder.Services.AddScoped<IPlayerRepository, PlayerRepository>();
51-
builder.Services.AddScoped<IPlayerService, PlayerService>();
38+
builder.Services.RegisterPlayerService();
5239
builder.Services.AddMemoryCache();
53-
builder.Services.AddHealthChecks();
40+
builder.Services.AddMappings();
5441

55-
/* AutoMapper --------------------------------------------------------------- */
56-
builder.Services.AddAutoMapper(typeof(PlayerMappingProfile));
42+
/* Repositories ------------------------------------------------------------- */
5743

58-
/* FluentValidation --------------------------------------------------------- */
59-
builder.Services.AddScoped<IValidator<PlayerRequestModel>, PlayerRequestModelValidator>();
44+
builder.Services.RegisterPlayerRepository();
6045

61-
if (builder.Environment.IsDevelopment())
62-
{
63-
/* Swagger UI ----------------------------------------------------------- */
64-
// https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle
65-
builder.Services.AddSwaggerGen(options =>
66-
{
67-
options.SwaggerDoc("v1", builder.Configuration.GetSection("SwaggerDoc").Get<OpenApiInfo>());
68-
options.IncludeXmlComments(SwaggerGenDefaults.ConfigureXmlCommentsFilePath());
69-
options.AddSecurityDefinition("Bearer", SwaggerGenDefaults.ConfigureSecurityDefinition());
70-
options.OperationFilter<AuthorizeCheckOperationFilter>();
71-
});
72-
}
46+
/* Data --------------------------------------------------------------------- */
47+
48+
builder.Services.AddDbContextPoolWithSqlite(builder.Environment);
7349

7450
var app = builder.Build();
7551

7652
/* -----------------------------------------------------------------------------
7753
* Middlewares
7854
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware
7955
* -------------------------------------------------------------------------- */
56+
8057
app.UseSerilogRequestLogging();
8158

8259
if (app.Environment.IsDevelopment())
@@ -85,16 +62,10 @@
8562
app.UseSwaggerUI();
8663
}
8764

88-
// https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl
8965
app.UseHttpsRedirection();
90-
91-
// https://learn.microsoft.com/en-us/aspnet/core/security/cors
9266
app.UseCors();
93-
94-
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#endpoints
95-
app.MapControllers();
96-
97-
// https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
67+
app.UseRateLimiter();
9868
app.MapHealthChecks("/health");
69+
app.MapControllers();
9970

10071
await app.RunAsync();

0 commit comments

Comments
 (0)