Skip to content

Commit 965769c

Browse files
committed
extensions added
1 parent 239164e commit 965769c

File tree

5 files changed

+315
-220
lines changed

5 files changed

+315
-220
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This project is a boilerplate for building .NET API applications with various fe
1919
- [x] [Middlewares](https://github.com/FullstackCodingGuy/dotnetapi-boilerplate/tree/main/src/Middlewares)
2020
- [ ] Entity Framework
2121
- [ ] Serilog
22+
- [ ] FluentValidation
2223
- [ ] Vault Integration
2324
- [ ] MQ Integration
2425
- [ ] Application Resiliency
@@ -53,16 +54,21 @@ This project is a boilerplate for building .NET API applications with various fe
5354
dotnet restore
5455
```
5556

56-
3. Update the `appsettings.json` and `serilog.json` files with your configuration.
57+
3. Update the `appsettings.json` and `appsettings.Development.json` files with your configuration.
5758

5859
### Running the Application
5960

6061
1. Build and run the application:
6162
```sh
6263
dotnet run
64+
65+
or
66+
67+
dotnet run --launch-profile "https"
68+
6369
```
6470

65-
2. The application will be available at `http://localhost:5035` (or the configured URL).
71+
2. The application will be available at `http://localhost:7000` and `https://localhost:7001` (or the configured URL).
6672

6773
### Health Check Endpoint
6874

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
}

src/Extensions/WebAppExtensions.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Globalization;
3+
using Microsoft.AspNetCore.Builder;
4+
using Serilog;
5+
6+
[ExcludeFromCodeCoverage]
7+
public static class WebAppExtensions
8+
{
9+
public static WebApplication ConfigureApplication(this WebApplication app)
10+
{
11+
12+
var IsDevelopment = app.Environment.IsDevelopment();
13+
14+
// ✅ Use Serilog Request Logging Middleware
15+
app.UseSerilogRequestLogging();
16+
17+
// Apply Authentication Middleware
18+
app.UseAuthentication();
19+
app.UseAuthorization();
20+
21+
// -----------------------------------------------------------------------------------------
22+
23+
if (IsDevelopment)
24+
{
25+
app.UseSwagger();
26+
app.UseSwaggerUI();
27+
}
28+
29+
// ✅ Use CORS Middleware before controllers
30+
app.UseCors(IsDevelopment ? "AllowAll" : "AllowSpecificOrigins"); // Apply the selected CORS policy
31+
32+
app.UseResponseCaching();
33+
34+
app.UseHttpsRedirection();
35+
app.MapControllers();
36+
37+
// Console.WriteLine(app.Environment.IsDevelopment().ToString());
38+
app.UseResponseCompression();
39+
40+
// use rate limiter
41+
app.UseRateLimiter();
42+
43+
// Ensure Database is Created
44+
// using (var scope = app.Services.CreateScope())
45+
// {
46+
// var dbContext = scope.ServiceProvider.GetRequiredService<ExpenseDbContext>();
47+
// dbContext.Database.Migrate();
48+
// }
49+
50+
51+
// Prevent Cross-Site Scripting (XSS) & Clickjacking
52+
// Use Content Security Policy (CSP) and X-Frame-Options:
53+
54+
app.Use(async (context, next) =>
55+
{
56+
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
57+
context.Response.Headers.Append("X-Frame-Options", "DENY");
58+
context.Response.Headers.Append("Content-Security-Policy", "default-src 'self'");
59+
await next();
60+
});
61+
62+
63+
app.MapGet("/", () => "Hello, World!");
64+
app.MapGet("/health", () => "Healthy");
65+
66+
app.MapGet("/secure", () => "You are authenticated!")
67+
.RequireAuthorization(); // Protect this endpoint
68+
69+
app.MapGet("/admin", () => "Welcome Admin!")
70+
.RequireAuthorization(policy => policy.RequireRole("admin"));
71+
72+
73+
74+
#region MinimalApi
75+
76+
// _ = app.MapVersionEndpoints();
77+
// _ = app.MapAuthorEndpoints();
78+
// _ = app.MapMovieEndpoints();
79+
// _ = app.MapReviewEndpoints();
80+
81+
#endregion MinimalApi
82+
83+
return app;
84+
}
85+
}

src/NetAPI.csproj

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

99
<ItemGroup>
10+
<PackageReference Include="FluentValidation" Version="11.11.0" />
1011
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.2" />
1112
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
1213
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />

0 commit comments

Comments
 (0)