CSharp - Other

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.

ValidateAntiForgeryToken in ASP.NET Core

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Enable anti-forgery middleware
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.Use(async (context, next) =>
{
    // Generates an anti-forgery token
    var antiForgery = context.RequestServices.GetRequiredService();
    var tokens = antiForgery.GetTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
    
    await next();
});

// Configure endpoints
app.MapControllers();
app.Run();


Controller

[HttpPost]
[ValidateAntiForgeryToken] // Validates the anti-forgery token
public IActionResult SubmitForm(string someInput)
{
    // Process the form data
    return RedirectToAction("Index");
}


Razor View

@Html.AntiForgeryToken() 


AJAX Requests

$.ajax({
    type: "POST",
    url: "/MyController/SubmitForm",
    data: {
        someInput: "value"
    },
    headers: {
        "RequestVerificationToken": $('input[name="__RequestVerificationToken"]').val() // Include the token
    },
    success: function (response) {
        // Handle success
    }
});

Validating Connection Strings on .NET Startup

Initial Host Configuration

using System.Data.Common;
using Microsoft.Data.SqlClient;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using WorkerServiceDatabase;


IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services 
            // validate connection strings
            .ValidateConnectionStrings()
            .ValidateOnStart();
        
        services.AddHostedService<Worker>();
    })
    .Build();


host.Run();


public static class ConnectionStringExtensions
{
    public static OptionsBuilder<ConnectionStrings> 
        ValidateConnectionStrings(this IServiceCollection services)
    {
        return services
            .AddOptions<ConnectionStrings>()
            .BindConfiguration("ConnectionStrings")
            .Validate(c => c.Validate(), "Could not connect to 1 or more databases.");
    }
}


public class ConnectionStrings
    : Dictionary<string,string>
{
    public ConnectionStrings()
    {
        // these are the key names
        DbProviderFactories.RegisterFactory("Sqlite", SqliteFactory.Instance);
        DbProviderFactories.RegisterFactory("SqlServer", SqlClientFactory.Instance);
    }
    
    public bool Validate()
    {
        // can't inject logger :(
        var logger = LoggerFactory
            .Create(cfg => cfg.AddConsole().AddDebug())
            .CreateLogger("ConnectionStrings");
        
        List<Exception> errors = new();
        foreach (var (key, connectionString) in this)
        {
            try
            {
                var factory = DbProviderFactories.GetFactory(key);
                using var connection = factory.CreateConnection();
                if (connection is null) {
                    throw new Exception($"\"{key}\" did not have a valid database provider registered");
                }


                connection.ConnectionString = connectionString;
                connection.Open();
            }
            catch (Exception e)
            {
                var message = $"Could not connect to \"{key}\".";
                logger.LogError(message);
                errors.Add(new Exception(message, e));
            }
        }


        return errors.IsNullOrEmpty();
    }
}


ConnectionString Configuration

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
      "Sqlite": "Data Source=database.db",
      "SqlServer": "Data Source=localhost,11433;Initial Catalog=DBName;User Id=sa;Password=password;Encrypt=false"
    }
}

Validation failed for one or more entities. See 'EntityValidationErrors' property for more details [duplicate]

Just add ((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors in a Quick Watch window and click Reevaluate.

ValueTask - Reduces Memory Allocations

ValueTask<T> avoids unnecessary heap allocations compared to Task<T>.


Before (More Allocations)

Returning Task<T> always causes heap allocation, even when returning a cached result.

public async Task<int> GetValueAsync()
{
    return await Task.FromResult(42); // Creates unnecessary Task object
}


After (Reduced Allocations with ValueTask<T>)

Using ValueTask<T>, allocations are avoided for completed tasks.

public ValueTask<int> GetValueAsync()
{
    return new ValueTask<int>(42); // No heap allocation
}


What .NET collection provides the fastest search

Working With Zip Files

Read Zip Files


List All Files Contained in the Zip File

using var zipFile = ZipFile.OpenRead("multi-folder.zip");

var counter = 0;
foreach (var entry in zipFile.Entries)
    Console.WriteLine($"{++counter,3}: {entry.Name}");


Extract Files From Zip File With ExtractToDirectory() Method

ZipFile.ExtractToDirectory("multi-folder.zip", "extracted-files", true);


Extract Files From Zip File With ExtractToFile() Method

using var zipFile = ZipFile.OpenRead("multi-folder.zip");
var rootFolder = "extracted-files";
foreach (var entry in zipFile.Entries)
{
    var wholePath = Path.Combine(
        rootFolder,
        Path.GetDirectoryName(entry.FullName) ?? string.Empty);

    if (!Directory.Exists(wholePath))
        Directory.CreateDirectory(wholePath);

    if (!string.IsNullOrEmpty(entry.Name))
    {
        var wholeFileName = Path.Combine(
            wholePath,
            Path.GetFileName(entry.FullName));

        entry.ExtractToFile(wholeFileName, true);
    }
}


Extract Files With Open() Method

using var zipFile = ZipFile.OpenRead("multi-folder.zip");
foreach (var entry in zipFile.Entries)
{
    if (!string.IsNullOrEmpty(entry.Name))
    {
        using (var stream = entry.Open())
        using (var memoryStream = new MemoryStream())
        {
            stream.CopyTo(memoryStream);
            var bytes = memoryStream.ToArray();
            var base64 = Convert.ToBase64String(bytes);
            Console.WriteLine($"{entry.FullName} => {base64}");
        }
    }
}


Create Zip Files


Compress (Zipping) Whole Folder

ZipFile.CreateFromDirectory(@"c:\temp", "temp.zip");


Add Files to a Zip File

var folder = new DirectoryInfo(".");
FileInfo[] files = folder.GetFiles("*.txt", SearchOption.AllDirectories);

using var archive = ZipFile.Open(@"..\parent.zip", ZipArchiveMode.Create);

foreach (var file in files)
{
    archive.CreateEntryFromFile(
        file.FullName,
        Path.GetRelativePath(folder.FullName, file.FullName)
    );
}


Create a Zip File With Stream

var helloText = "Hello world!";

using var archive = ZipFile.Open(@"..\test.zip", ZipArchiveMode.Create);

var entry = archive.CreateEntry("hello.txt");

using (Stream st = entry.Open())
using (StreamWriter writerManifest = new StreamWriter(st))
writerManifest.WriteLine(helloText);


Compression Level

var entry = archive.CreateEntry("hello.txt", CompressionLevel.SmallestSize);


Delete Entries From Zip Files


using var zipFile = ZipFile.Open("multi-folder.zip", ZipArchiveMode.Update);

var images = zipFile.Entries.Where(e => e.Name == "image.png").ToList();

for (int i = images.Count - 1; i >= 0; --i)
    images[i].Delete();