2
\$\begingroup\$

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();
\$\endgroup\$
2
  • 4
    \$\begingroup\$ This might be an opinion based question, which might be off topic. But for your sake, Program.cs does not need to have comments on every line, most lines are self explanatory. If you want to keep your Program.cs better, you need to start using extension methods instead to keep your configuration intact, and to minimize the modifications on Program.cs as some services are order-sensitive. \$\endgroup\$ Commented Apr 20 at 19:34
  • 3
    \$\begingroup\$ The current title of your question is too generic to be helpful. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How do I ask a good question?. \$\endgroup\$ Commented May 18 at 11:50

1 Answer 1

3
\$\begingroup\$

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.
  • Try to avoid app.Use to 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:
    • Configuration
    • apiKey
    • appName
    • ...
    • 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)
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.