\$\begingroup\$
\$\endgroup\$
2
I use this code as reference for when I start working on a new project whether that be for personal or for work. It's worked fine so far, but I'm wondering if anyone can read it and provide any recommendations or can see if there are any serious oversights I've missed. This has only been reviewed by ChatGPT so far over the past year haha.
// Builder allows you to load these 3 things mainly:
// builder.Environment, builder.Configuration, and builder.Services
var builder = WebApplication.CreateBuilder(args);
// To later tells app where its running (dev or prod)
IWebHostEnvironment environment = builder.Environment;
// For URL Rewriting
RewriteOptions rewriteOptions = new();
// Configure controllers for the mvc pipeline
builder.Services.AddControllersWithViews();
// Use to get CORS Values later on from the appsettings
string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
// Access to appsettings and other config.
var Configuration = builder.Configuration;
// Allows reading from env vars great for docker or ci/cd secrets
builder.Configuration.AddEnvironmentVariables();
// Other things you can do with builder.Configuration:
var apiKey = builder.Configuration.GetValue<string>("AppSettings:ApiKey");
var appName = builder.Configuration.GetValue<string>("AppSettings:AppName");
// You can also check which environment you are in using builder.Environment:
// but we are already doing this later on using the environment variable
// so this is just an example not to be used
if (builder.Environment.IsDevelopment())
{
Console.WriteLine("We are in Development environment.");
}
else if (builder.Environment.IsProduction())
{
Console.WriteLine("We are in Production environment.");
}
else
{
Console.WriteLine("We are in another environment.");
}
// Enable app to support multiple languages, Lookup IStringLocalizer
// Which allows us to use RESX files
builder.Services.AddLocalization();
// Add session support to app which will add a cookie to the browser
// containing session id and session name that links to this session
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(20); //sets timeout
options.Cookie.Name = "ProjectName";
/*
Helps prevents cross site request forgery "CSRF"
meaning: session can only be used in the same website it was created in
so it prevents malicious actors from different websites from
manipulating user to activate links to our website that could be harmful
for example bank.com / pay ? id = 12 & value = 1000
if no samesitemode.strict, the link can be clicked on by the user on hackerWebsite.com
and it will execute because hackerWebsite.com will have access to the session
*/
options.Cookie.SameSite = SameSiteMode.Strict;
// Make cookie inaccessible by javascript/browser
// thus it prevents cookies from being stolen through XSS attacks
// XSS attack: hacker injects malicious js to our website such as
// logging keystrokes or stealing cookies
options.Cookie.HttpOnly = true;
// if request is https, cookie will be secure (only sent on https)
// if request is http, cookie not secure
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
// marks cookie as essential
// even if user rejects cookies this is still needed to work
options.Cookie.IsEssential = true;
});
// Protect against CSRF attacks
// by adding request verification tokens
// this allows us to use the [ValidateAntiForgeryToken] attribute on actions
builder.Services.AddAntiforgery(options =>
{
// Prevent request verification tokens from working inside iframes
options.SuppressXFrameOptionsHeader = true; options.Cookie.Name = "ProjectNameaf";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
// Global cookie policy to always be used on all generated cookies by default
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always;
options.Secure = CookieSecurePolicy.SameAsRequest;
});
// Configure self hosted web server
// important to take this into account if project will use Docker
// advise reading more into Kestrel and Reverse Proxy Servers
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.AddServerHeader = false;
serverOptions.Limits.MaxRequestBodySize = 409600 * 1024;
serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(240);
});
// Compress response data before sending it back to client
// to reduce size and for faster loading times
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
// Newer algorithm than gzip and more efficient
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
// Specify the types of data being sent FROM our server as a response
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] {
"text/*",
"message/*",
"image/*",
"application/javascript",
"font/woff",
"font/woff2",
"font/opentype",
"font/ttf",
"video/mp4",
"application/octet-stream"
});
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
// Set to highest possible minimization this will require more processing power
options.Level = CompressionLevel.SmallestSize;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.SmallestSize;
});
// Lets you access the httpcontext outside of controllers
// http context contains request + response
// Encapsulates all info about a single http request and response pair
builder.Services.AddHttpContextAccessor();
// Create local cache not shared between different application instances
// i.e IMemoryCache
builder.Services.AddMemoryCache();
// Opposite of above for in-memory cache across different instances of app
// Not like Redis which stores persistent caching, this is not persistent (local)
// i.e IDistributedCache
builder.Services.AddDistributedMemoryCache();
//ADD ALL YOUR SERVICES AND EXTERNAL SERVICES HERE
// Set up caching for image processing
// Cache the transformed image (resized image)
builder.Services.AddImageflowHybridCache(new HybridCacheOptions(Path.Combine(environment.WebRootPath, "imagecache")));
// Set up CORS
if (!string.IsNullOrEmpty(myAllowedOrigins))
{
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins, policy =>
{
policy.WithOrigins(myAllowedOrigins);
});
options.AddPolicy("AllowInstagram", policy =>
{
policy.WithOrigins("https://www.instagram.com").AllowAnyMethod().AllowAnyHeader();
});
});
}
// Gives an instance of WebApp through which we configure middlewares
var app = builder.Build();
// Middleware for some cross origin stuff and some other stuff
// context: http context
// next: next middleware
app.Use(async (context, next) =>
{
//THESE ARE mostly CORS STUFF
// Only allow iframes of same origin
context.Response.Headers["X-Frame-Options"] = "SAMEORIGIN";
// Prevent browsers from interpreting files as different mime type
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
// Enable XSS filter in browser to block pages when XSS is detected
context.Response.Headers["X-Xss-Protection"] = "1; mode=block";
context.Response.Headers["X-Permitted-Cross-Domain-Policies"] = "master-only";
// Set up CORS
context.Response.Headers["Access-Control-Allow-Origin"] = myAllowedOrigins;
// Specify which http headers can be used when making a CROSS ORIGIN request
context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type";
// Set methods only for cross origin requests
context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST";
// Allow browser to include cookies and http authentication inside the requests
context.Response.Headers["Access-Control-Allow-Credentials"] = "true";
// Activates HSTS that tells web browser to enforce the use of https for all future requests
// but first request can be http to establish communication
context.Response.Headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload";
// When request from websiteB to websiteA,
// if website A is https, send FULL url of websiteB in request header
// if website A is http, don't send url
// default is strict-origin-when-cross-origin, which will send the ORIGIN URL only not full
context.Response.Headers["Referrer-Policy"] = "no-referrer-when-downgrade";
// Define which browser features such as camera, microphone, and geolocation
// can be used in cross frame requests
context.Response.Headers["Feature-Policy"] = "accelerometer 'none'; camera 'none'; gyroscope 'none'; magnetometer 'none'; " +
"microphone 'none'; payment 'none'; usb 'none'";
// Control what content can load to the browser
context.Response.Headers["Content-Security-Policy"] = "script-src 'self' 'unsafe-inline' 'unsafe-eval' *.instagram.com *.google-analytics.com *.googleapis.com *.googletagmanager.com *.gstatic.com *.google.com *.doubleclick.net *.cloudflare.com *.azure.com *.windows.net *.telerik.com *.getboostrap.com *.gstatic.com *.jqueryui.com;style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com; frame-ancestors 'self' https://www.instagram.com;";
var allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
".htm",".shtm",".ashx",".rem",".stm",".html",".shtml",".css",".js",".jpg",".jpeg",".png",".gif",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".pdf",".woff",".woff2",".eot",".ttf",".otf",".svg",".map",".txt",".xml",".ico",".json",".mp4"
};
var extension = Path.GetExtension(context.Request.Path);
if (!string.IsNullOrEmpty(extension) && !allowedExtensions.Contains(extension))
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
var disallowedVerbs = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
"TRACE","HEAD","OPTIONS","PUT","DEBUG","PATCH","DELETE"
};
if (disallowedVerbs.Contains(context.Request.Method))
{
context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
return;
}
await next();
}
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Site/Error");
app.UseHsts();
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseStatusCodePagesWithRedirects("/errorpage.html?code={0}");
}
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == 405)
{
context.Request.Path = "/ErrorPage.html";
await next();
}
if (context.Response.StatusCode == 404)
{
context.Request.Path = "/en/page-not-found";
await next();
}
});
// Define custom MIME type mappings for specific file extensions
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".riv"] = "application/octet-stream";
provider.Mappings[".woff"] = "font/woff";
provider.Mappings[".woff2"] = "font/woff2";
provider.Mappings[".otf"] = "font/opentype";
provider.Mappings[".ttf"] = "font/ttf";
provider.Mappings[".mp4"] = "video/mp4";
provider.Mappings[".svg"] = "image/svg+xml";
var cacheMaxAgeSixMonths = (60 * 60 * 24 * 30 * 6).ToString();
app.UseHttpsRedirection();
// Allow browser to request files
// ALWAYS Before UseRouting
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append("Cache-Control", $ "public, max-age={cacheMaxAgeSixMonths}");
}
});
// Compression
app.UseResponseCompression();
// Analyze incoming requests to determine the endpoint that matches the route
// Any middleware after this will know which endpoint will run eventually
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins); //or instagram cors, you can select by name
//Activate sessions for app
app.UseSession();
// NOTE THAT Authentication and authorization must always be BETWEEN UseRouting and MapControllers
// Allows us to use authentication methods like jwt
// i.e User.Claims and User.Identity
app.UseAuthentication();
// Adds ability to add [Authorize] to resources (classes + functions)
app.UseAuthorization();
//ADD CUSTOM MIDDLEWARE HERE
// For bundling, this is considered custom
app.UseWebOptimizer();
// Also considered custom
rewriteOptions.Rules.Add(new FriendlyUrlRouteHandler());
app.UseRewriter(rewriteOptions);
// Map any razor pages you have if any
app.MapRazorPages();
// Actually maps our controller routes to endpoints
app.MapControllers();
// Adds default controller route
app.MapControllerRoute(
name: "default",
pattern: "{controller=Site}/{action=Index}/{id?}");
app.Run();
toolic
15.9k6 gold badges29 silver badges217 bronze badges
1 Answer 1
\$\begingroup\$
\$\endgroup\$
Comments
- Try to avoid commenting the what and the how
- This is just basically echos the code itself
- Try to capture the why and why not decisions
- This preserves context, why option A has been chosen over option B, C
Use encapsulation
- Create functions that bundle related code fragements
- Like
ConfigureCORS,ConfigureCompression,ConfigureSession, etc.
- Like
- Try to avoid
app.Useto do define in-line middleware- Create dedicated classes for them and register them into the app
Clear unused code
- You have created several variables, and you have never used them
- Just to name a few:
ConfigurationapiKeyappName- ...
provider
- If you don't need them, then delete them
Magic numbers and number literals
- Do not use magic numbers without providing context, like
serverOptions.Limits.MaxRequestBodySize = 409600 * 1024;
- Try to ease the interpretation of the numbers
//from
serverOptions.Limits.MaxRequestBodySize = 409600 * 1024;
//to
serverOptions.Limits.MaxRequestBodySize = 400 * 1_024 * 1_024;
or
//from
serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(240);
//to
serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(4);
- Prefer using enums for improve legibility
//from
if (context.Response.StatusCode == 405)
//to
if (context.Response.StatusCode == (int)HttpStatusCode.MethodNotAllowed)
answered May 16 at 13:58
Peter Csala
10.8k1 gold badge16 silver badges36 bronze badges
You must log in to answer this question.
Explore related questions
See similar questions with these tags.
lang-cs
Program.csdoes not need to have comments on every line, most lines are self explanatory. If you want to keep yourProgram.csbetter, you need to start using extension methods instead to keep your configuration intact, and to minimize the modifications onProgram.csas some services areorder-sensitive. \$\endgroup\$