CSharp - Other

Uploading Large Files in ASP.NET Core

Enabling Kestrel Support for the Large Files

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxRequestBodySize = long.MaxValue;
});


public class FileUploadSummary
{
    public int TotalFilesUploaded { get; set; }
    public string TotalSizeUploaded { get; set; }
    public IList<string> FilePaths { get; set; } = new List<string>();
    public IList<string> NotUploadedFiles { get; set; } = new List<string>();
}


[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class MultipartFormDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var request = context.HttpContext.Request;

        if (request.HasFormContentType 
            && request.ContentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        context.Result = new StatusCodeResult(StatusCodes.Status415UnsupportedMediaType);
    }
}


Upload Large Files Using Streams

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}


[HttpPost("upload-stream-multipartreader")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)]
[MultipartFormData]
[DisableFormValueModelBinding]
public async Task<IActionResult> Upload()
{
    var fileUploadSummary = await _fileService.UploadFileAsync(HttpContext.Request.Body, Request.ContentType);

    return CreatedAtAction(nameof(Upload), fileUploadSummary);
}


public async Task<FileUploadSummary> UploadFileAsync(Stream fileStream, string contentType)
{
    var fileCount = 0;
    long totalSizeInBytes = 0;

    var boundary = GetBoundary(MediaTypeHeaderValue.Parse(contentType));
    var multipartReader = new MultipartReader(boundary, fileStream);
    var section = await multipartReader.ReadNextSectionAsync();

    var filePaths = new List<string>();
    var notUploadedFiles = new List<string>();
    
    while (section != null)
    {
        var fileSection = section.AsFileSection();
        if (fileSection != null)
        {
            totalSizeInBytes += await SaveFileAsync(fileSection, filePaths, notUploadedFiles);
            fileCount++;
        }

        section = await multipartReader.ReadNextSectionAsync();
    }

    return new FileUploadSummary
    {
        TotalFilesUploaded = fileCount,
        TotalSizeUploaded = ConvertSizeToString(totalSizeInBytes),
        FilePaths = filePaths,
        NotUploadedFiles = notUploadedFiles
    };
}

Use AsNoTracking for Read-Only Operation in LINQ

AsNoTracking() is a simple way to make your read-only queries faster and more efficient by skipping the change tracking feature of Entity Framework.

using (var context = new MyDbContext())
{
    var customers = context.Customers
                           .AsNoTracking()
                           .Where(c => c.IsActive)
                           .ToList();
}

Use Enum Instead of Hard-Coded Numbers

Do

enum Months { 
  January = 1, 
  February = 2, 
  March = 3, 
  April = 4, 
  May = 5, 
  June = 6, 
  July = 7, 
  August = 8, 
  September = 9, 
  October = 10, 
  November = 11,
  December = 12 
} 

if((Months)month == Months.February) { 
  days = 28; 
}


Don't

if(month == 2) { 
  days = 28; 
}

Use GroupBy() with ToLookup() for Faster Lookups

GroupBy() is powerful but can be inefficient for multiple lookups. ToLookup() offers better performance for repeated accesses.


Problem:

var groups = numbers.GroupBy(n => n % 2);
var evens = groups.First(g => g.Key == 0);
//This iterates multiple times.


Solution:

var lookup = numbers.ToLookup(n => n % 2);
var evens = lookup[0];This avoids unnecessary iterations.

Use Null Coalescing Operator

Do

public Book GetTheBestBook(Book book) { 
  return book ?? new Book () { Name = "C# in Depth" }; 
}


Don't

public Book GetTheBestBook(Book book) { 
  if (book != null) { 
    return book; 
  } 
  else { 
    return new Book() { Name = "C# in Depth" }; 
  } 
}

Use String Interpolation

Do

public string GetTheBestBookName(Book book) { 
  return $"The Best book's Name is {book.Name}. and the author name is {book.Author}"; 
}


Don't

public string GetTheBestBookName(Book book) { 
  return "The Best book's Name is " + book.Name + " and the author name is " + book.Author; 
}

Use struct instead of class for value types that have a small memory footprint

Do

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}


Don't

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Use Ternary Operator Instead of If Else

Do

return age >= 18 ? "You are an adult" : "You are not an adult";


Don't

if (age >= 18) { 
  return "You are an adult"; 
} 
else { 
  return "You are not an adult"; 
}

Use the string.Format method instead of concatenating strings with the + operator

Do

string firstName = "John";
string lastName = "Doe";
string fullName = string.Format("{0} {1}", firstName, lastName);


Don't

string firstName = "John";
string lastName = "Doe";
string fullName = firstName + " " + lastName;

Use the StringBuilder class for concatenating large numbers of strings

Do

StringBuilder result = new StringBuilder();
for (int i = 0; i < 100; i++)
{
    result.Append(i.ToString());
}


Don't

string result = "";
for (int i = 0; i < 100; i++)
{
    result += i.ToString();
}

Use the using statement to properly dispose of resources

Do

using (SqlConnection connection = new SqlConnection("connection string"))
{
    connection.Open();
    // Do something with the connection
}


Don't

SqlConnection connection = new SqlConnection("connection string");
connection.Open();
// Do something with the connection
connection.Close();

Using SemaphoreSlim to Limit API Calls Per Minute

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;


class Program
{
    private static readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1); // Allows one task at a time to update the counter
    private static readonly int MaxCallsPerMinute = 100;
    private static readonly TimeSpan RateLimitInterval = TimeSpan.FromMinutes(1);


    private static int CallsMade = 0;
    private static DateTime RateLimitReset = DateTime.UtcNow.Add(RateLimitInterval);


    static async Task Main(string[] args)
    {
        string apiUrl = "https://api.example.com/endpoint";


        // Simulate multiple API calls
        var tasks = new Task[200]; // Simulate 200 API requests


        for (int i = 0; i < tasks.Length; i++)
        {
            int requestId = i + 1;
            tasks[i] = MakeRateLimitedCall(apiUrl, requestId);
        }


        // Wait for all tasks to complete
        await Task.WhenAll(tasks);


        Console.WriteLine("All API calls completed.");
    }


    private static async Task MakeRateLimitedCall(string apiUrl, int requestId)
    {
        // Enforce rate-limiting logic
        await Semaphore.WaitAsync();
        try
        {
            await ApplyRateLimit(requestId);


            // Simulate API call
            using (HttpClient client = new HttpClient())
            {
                Console.WriteLine($"Request {requestId}: Started.");
                HttpResponseMessage response = await client.GetAsync(apiUrl);
                Console.WriteLine($"Request {requestId}: Completed with status {response.StatusCode}.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Request {requestId}: Error - {ex.Message}");
        }
        finally
        {
            Semaphore.Release();
        }
    }


    private static Task ApplyRateLimit(int requestId)
    {
        lock (Semaphore)
        {
            if (DateTime.UtcNow >= RateLimitReset)
            {
                // Reset the counters when the time window is up
                CallsMade = 0;
                RateLimitReset = DateTime.UtcNow.Add(RateLimitInterval);
                Console.WriteLine($"Request {requestId}: Rate limit reset.");
            }


            if (CallsMade >= MaxCallsPerMinute)
            {
                var waitTime = (int)(RateLimitReset - DateTime.UtcNow).TotalMilliseconds;
                Console.WriteLine($"Request {requestId}: Rate limit reached, waiting {waitTime / 1000} seconds...");
                Monitor.Wait(Semaphore, waitTime);
            }


            CallsMade++;
        }


        return Task.CompletedTask;
    }
}