CSharp - Other

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;
    }
}


Using SemaphoreSlim to limit concurrent API calls

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


class Program
{
    // Semaphore to limit concurrent calls
    private static readonly SemaphoreSlim Semaphore = new SemaphoreSlim(5); // Maximum 5 concurrent calls


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


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


        for (int i = 0; i < tasks.Length; i++)
        {
            int requestId = i + 1; // Capture the request ID
            tasks[i] = MakeApiCallAsync(apiUrl, requestId);
        }


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


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


    private static async Task MakeApiCallAsync(string apiUrl, int requestId)
    {
        await Semaphore.WaitAsync(); // Wait for a slot to be available


        try
        {
            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(); // Release the semaphore slot
        }
    }
}


Using System.Text.Json for Camel Case Serialization

Use JsonPropertyName Attribute for Camel Case Serialization

public class PersonWithAttributes
{
    [JsonPropertyName("firstName")]
    public string? FirstName { get; set; }
    
    [JsonPropertyName("surname")]
    public string? Surname { get; set; }
    
    [JsonPropertyName("age")]
    public int? Age { get; set; }
    
    [JsonPropertyName("isActive")]
    public bool? IsActive { get; set; }
}


Create JsonSerializerExtensions

public static class JsonSerializerExtensions
{
    public static string SerializeWithCamelCase<T>(this T data)
    {
        return JsonSerializer.Serialize(data, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });
    }

    public static T DeserializeFromCamelCase<T>(this string json)
    {
        return JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });
    }
}


How to use ?

var personWithAttributes = new PersonWithAttributes
{
    Age = 20,
    FirstName = "John",
    Surname = "Doe",
    IsActive = true
};

JsonSerializer.Serialize(personWithAttributes, new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});

or

personWithAttributes.SerializeWithCamelCase();

or

personWithAttributes.DeserializeFromCamelCase<PersonWithAttributes>()

UUID vs. ULID

Unique identifiers are essential for managing records in modern applications, particularly in distributed or large-scale systems. UUIDs (Universally Unique Identifiers) and ULIDs (Universally Unique Lexicographically Sortable Identifiers) are two popular choices, each offering unique benefits. Let's explore what makes them different, their pros and cons, and how to choose the best one for your specific needs.


Understanding UUIDs


What is a UUID?


A UUID is a 128-bit identifier designed to ensure global uniqueness. They are often used in databases, systems, and applications to uniquely identify records or objects.


  • Format: Typically displayed as a 36-character string composed of 32 hexadecimal digits separated by hyphens (e.g., 123e4567-e89b-12d3-a456-426614174000).
  • Uniqueness: UUIDs are designed to minimize collisions, making them suitable for distributed systems where records or objects are generated independently across multiple nodes or services.


Advantages of UUIDs


  1. Global Uniqueness: UUIDs provide a high level of uniqueness, ideal for applications where records are created across multiple systems or distributed databases.
  2. Standardization: UUIDs follow a standardized format (RFC 4122) and are widely supported across various programming languages and systems.
  3. Decentralization: They can be generated independently by different systems without requiring a central authority.


Disadvantages of UUIDs


  1. Non-Sortable: UUIDs are not inherently sortable by creation time, which can complicate operations that require chronological ordering.
  2. Size and Storage: UUIDs are relatively large compared to integer-based IDs, which can impact storage and indexing performance.
  3. Human Readability: UUIDs are long and not human-friendly, making them less suitable for debugging or scenarios where identifiers are exposed to end-users.


Understanding ULIDs


What is a ULID?


A ULID is a 128-bit identifier that combines uniqueness with sortability by creation time. ULIDs were designed to address some of the limitations of UUIDs, particularly their lack of temporal order.


  • Format: Typically displayed as a 26-character string composed of alphanumeric characters (e.g., 01F8M2AYYJAZ1RC4GSF3VXG9J5).
  • Sortability: ULIDs are lexicographically sortable, meaning they can be ordered chronologically, which is particularly useful in databases and logs.


Advantages of ULIDs


  1. Sortable by Time: ULIDs are designed to be sortable, allowing for easy chronological ordering of records.
  2. Global Uniqueness: Like UUIDs, ULIDs provide global uniqueness, making them suitable for distributed environments.
  3. Compact and Readable: ULIDs are shorter and more readable than UUIDs, making them slightly more user-friendly.


Disadvantages of ULIDs


  1. Limited Adoption: ULIDs are relatively new and may not be as widely supported or documented as UUIDs.
  2. Potential Collision Risk: While minimal, there is a slightly higher risk of collisions if multiple ULIDs are generated within the same millisecond.
  3. Compatibility Issues: Some systems or libraries may not fully support ULIDs, requiring additional implementation work.


Real-World Use Cases


When to Use UUIDs


  1. Distributed Systems: Ideal for applications where data is generated across multiple nodes or services, such as in microservices architectures.
  2. APIs and Integrations: When interacting with external systems that expect UUIDs, maintaining compatibility is easier with standardized UUIDs.
  3. Data Replication and Synchronization: UUIDs are useful for ensuring uniqueness in databases that replicate across multiple servers or regions.


When to Use ULIDs


  1. Event Sourcing: ULIDs are well-suited for event logs where chronological ordering of events is important.
  2. E-Commerce and Transactions: In systems where orders, transactions, or events need to be sorted by creation time, ULIDs offer a great balance of uniqueness and order.
  3. Readable Identifiers: Applications that require more human-friendly or compact identifiers benefit from ULIDs.


Performance Comparison: UUID vs. ULID


Storage and Indexing


  • UUIDs: Due to their random nature, UUIDs can impact indexing performance and storage efficiency in databases, especially for large datasets.
  • ULIDs: Provide better indexing performance for time-based queries due to their sortable nature, making them ideal for applications that require both uniqueness and temporal order.


Sorting Operations


  • UUIDs: Sorting UUIDs can be slower because they lack inherent order.
  • ULIDs: Naturally sortable, making them more efficient for operations that require records to be sorted by creation time.


Choosing the Right Identifier for Your Project


Choose UUID If:


  • You need a globally unique identifier and don't require sorting by creation time.
  • Your system must comply with established standards or third-party integrations that require UUIDs.
  • You're working with a decentralized architecture where minimizing collision risks is crucial.


Choose ULID If:


  • Your application requires both uniqueness and chronological ordering of records.
  • You need identifiers that are more readable or user-friendly.
  • You're building event-driven systems or applications where the order of events is important.


Conclusion


Both UUIDs and ULIDs offer unique benefits and are suitable for different scenarios. While UUIDs provide robust uniqueness for distributed systems, ULIDs offer an additional advantage of time-based sorting. The choice between UUIDs and ULIDs depends on your specific needs, such as the importance of ordering, compatibility requirements, or readability.