1+ using System . Diagnostics . CodeAnalysis ;
2+ using System . Globalization ;
3+ using System . Reflection ;
4+ using System . Text . Json ;
5+ using System . Text . Json . Serialization ;
6+ using FluentValidation ;
7+ using Microsoft . AspNetCore . Builder ;
8+ using Microsoft . AspNetCore . Http . Json ;
9+ using Microsoft . Extensions . DependencyInjection ;
10+ using Microsoft . OpenApi . Models ;
11+ using Serilog ;
12+ using Microsoft . AspNetCore . Builder ;
13+ using Microsoft . AspNetCore . Hosting ;
14+ using Microsoft . Extensions . DependencyInjection ;
15+ using Microsoft . Extensions . Hosting ;
16+ using System ;
17+ using Microsoft . EntityFrameworkCore ;
18+ using System . Threading . RateLimiting ;
19+ using Microsoft . AspNetCore . RateLimiting ;
20+ using Microsoft . AspNetCore . Http ;
21+ using Microsoft . Extensions . Configuration ;
22+ using Microsoft . AspNetCore . Authentication . JwtBearer ;
23+ using Microsoft . IdentityModel . Tokens ;
24+
25+ [ ExcludeFromCodeCoverage ]
26+ public static class WebAppBuilderExtension
27+ {
28+ public static WebApplicationBuilder ConfigureApplicationBuilder ( this WebApplicationBuilder builder )
29+ {
30+
31+
32+ var IsDevelopment = builder . Environment . IsDevelopment ( ) ;
33+
34+ #region ✅ Configure Serilog
35+ Log . Logger = new LoggerConfiguration ( )
36+ . WriteTo . Console ( ) // Log to console
37+ . WriteTo . File ( "logs/api-log.txt" , rollingInterval : RollingInterval . Day ) // Log to a file (daily rotation)
38+ //.WriteTo.Seq("http://localhost:5341") // Optional: Centralized logging with Seq
39+ . Enrich . FromLogContext ( )
40+ . MinimumLevel . Information ( )
41+ . CreateLogger ( ) ;
42+
43+ // ✅ Replace default logging with Serilog
44+ builder . Host . UseSerilog ( ) ;
45+
46+ // -----------------------------------------------------------------------------------------
47+ #endregion ✅ Configure Serilog
48+
49+
50+ // ✅ Use proper configuration access for connection string
51+ // var connectionString = configuration["ConnectionStrings:DefaultConnection"] ?? "Data Source=expensemanager.db";
52+
53+ // builder.Services.AddDbContext<ExpenseDbContext>(options =>
54+ // options.UseSqlite(connectionString));
55+
56+ builder . Services . AddResponseCaching ( ) ; // Enable Response caching
57+
58+ builder . Services . AddMemoryCache ( ) ; // Enable in-memory caching
59+
60+
61+ #region ✅ Authentication & Authorization
62+
63+ // Add Authentication
64+ builder . Services . AddAuthentication ( JwtBearerDefaults . AuthenticationScheme )
65+ . AddJwtBearer ( options =>
66+ {
67+ options . Authority = "http://localhost:8080/realms/master" ;
68+ options . Audience = "my-dotnet-api" ; // Must match the Keycloak Client ID
69+ options . RequireHttpsMetadata = false ; // Disable in development
70+ options . TokenValidationParameters = new TokenValidationParameters
71+ {
72+ ValidateIssuer = true ,
73+ ValidateAudience = true ,
74+ ValidateLifetime = true ,
75+ ValidAudiences = new string [ ] { "master-realm" , "account" , "my-dotnet-api" } ,
76+ ValidateIssuerSigningKey = true
77+ } ;
78+ } ) ;
79+
80+ builder . Services . AddAuthorization ( ) ;
81+ // -----------------------------------------------------------------------------------------
82+
83+
84+ #endregion
85+
86+ #region ✅ Serialisation
87+
88+ // Use System.Text.Json instead of Newtonsoft.Json for better performance.
89+
90+ // Enable reference handling and lower casing for smaller responses:
91+ // Explanation
92+ // JsonNamingPolicy.CamelCase → Converts property names to camelCase (recommended for APIs).
93+ // ReferenceHandler.Preserve → Prevents circular reference issues when serializing related entities.
94+
95+
96+ _ = builder . Services . Configure < JsonOptions > ( opt =>
97+ {
98+ opt . SerializerOptions . ReferenceHandler = ReferenceHandler . Preserve ;
99+ opt . SerializerOptions . DefaultIgnoreCondition = JsonIgnoreCondition . WhenWritingNull ;
100+ opt . SerializerOptions . PropertyNameCaseInsensitive = true ;
101+ opt . SerializerOptions . PropertyNamingPolicy = JsonNamingPolicy . CamelCase ;
102+ opt . SerializerOptions . Converters . Add ( new JsonStringEnumConverter ( JsonNamingPolicy . CamelCase ) ) ;
103+ } ) ;
104+
105+ #endregion Serialisation
106+
107+ #region ✅ CORS Policy
108+
109+
110+ // ✅ Add CORS policy
111+ // WithOrigins("https://yourfrontend.com") → Restricts access to a specific frontend.
112+ // AllowAnyMethod() → Allows all HTTP methods (GET, POST, PUT, DELETE, etc.).
113+ // AllowAnyHeader() → Allows any request headers.
114+ // AllowCredentials() → Enables sending credentials like cookies, tokens (⚠️ Only works with a specific origin, not *).
115+ // AllowAnyOrigin() → Enables unrestricted access (only for local testing).
116+
117+ builder . Services . AddCors ( options =>
118+ {
119+ // To allow prod specific origins
120+ options . AddPolicy ( "AllowSpecificOrigins" , policy =>
121+ {
122+ policy . WithOrigins ( "https://yourfrontend.com" ) // Replace with your frontend URL
123+ . AllowAnyMethod ( )
124+ . AllowAnyHeader ( )
125+ . AllowCredentials ( ) ; // If authentication cookies/tokens are needed
126+ } ) ;
127+
128+ // To allow unrestricted access (only for non prod testing)
129+ options . AddPolicy ( "AllowAll" , policy =>
130+ {
131+ policy . AllowAnyOrigin ( ) // Use only for testing; NOT recommended for production
132+ . AllowAnyMethod ( )
133+ . AllowAnyHeader ( ) ;
134+ } ) ;
135+ } ) ;
136+
137+ // -----------------------------------------------------------------------------------------
138+
139+
140+ #endregion
141+
142+ #region ✅ Rate Limiting & Request Payload Threshold
143+
144+
145+ // Use Rate Limiting
146+ // Prevent API abuse by implementing rate limiting
147+ // Add Rate Limiting Middleware
148+ // Now, each client IP gets 10 requests per minute, with a queue of 2 extra requests.
149+
150+ builder . Services . AddRateLimiter ( options =>
151+ {
152+ options . RejectionStatusCode = StatusCodes . Status429TooManyRequests ; // Return 429 when limit is exceeded
153+
154+ // ✅ Explicitly specify type <string> for AddPolicy
155+ options . AddPolicy < string > ( "fixed" , httpContext =>
156+ RateLimitPartition . GetFixedWindowLimiter (
157+ partitionKey : httpContext . Connection . RemoteIpAddress ? . ToString ( ) ?? "default" ,
158+ factory : _ => new FixedWindowRateLimiterOptions
159+ {
160+ PermitLimit = 10 , // Allow 10 requests
161+ Window = TimeSpan . FromMinutes ( 1 ) , // Per 1-minute window
162+ QueueProcessingOrder = QueueProcessingOrder . OldestFirst ,
163+ QueueLimit = 2 // Allow 2 extra requests in queue
164+ } ) ) ;
165+ } ) ;
166+
167+ // Limit Request Size
168+ // Prevent DoS attacks by limiting payload size.
169+ // https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-9.0
170+ builder . WebHost . ConfigureKestrel ( serverOptions =>
171+ {
172+ serverOptions . Limits . MaxRequestBodySize = 100_000_000 ;
173+ } ) ;
174+
175+ // -----------------------------------------------------------------------------------------
176+
177+
178+ #endregion
179+
180+
181+
182+ builder . Services . AddControllers ( )
183+ . AddJsonOptions ( options =>
184+ {
185+ options . JsonSerializerOptions . PropertyNamingPolicy = JsonNamingPolicy . CamelCase ;
186+ options . JsonSerializerOptions . ReferenceHandler = ReferenceHandler . Preserve ;
187+ } ) ;
188+
189+ if ( IsDevelopment )
190+ {
191+ builder . Services . AddEndpointsApiExplorer ( ) ;
192+ builder . Services . AddSwaggerGen ( ) ;
193+ }
194+
195+ // Enable Compression to reduce payload size
196+ builder . Services . AddResponseCompression ( options =>
197+ {
198+ options . EnableForHttps = true ;
199+ } ) ;
200+
201+ return builder ;
202+ }
203+ }
0 commit comments