The practical bits

Keep this handy.

A small collection of C# patterns and working examples. Read, copy, and make them your own.

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureLogging( (context, builder) => builder.AddConsole())
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(options =>
        {
          	// gracefull shut down time
            options.ShutdownTimeout = TimeSpan.FromSeconds(30);
        });
        services.AddHostedService<Worker>();
    })
    .Build();

await host.RunAsync();
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public Worker(ILogger<Worker> logger, IHostApplicationLifetime hostApplicationLifetime)
    {
        _logger = logger;
        _hostApplicationLifetime = hostApplicationLifetime;
    }

    public override Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            // callback methods when host is gracefully shutting down the service
            _hostApplicationLifetime.ApplicationStarted.Register(() => _logger.LogInformation("started"));
            _hostApplicationLifetime.ApplicationStopping.Register(() => _logger.LogInformation("stopping"));
            _hostApplicationLifetime.ApplicationStopped.Register(() => _logger.LogInformation("stopped"));
            return base.StartAsync(cancellationToken);
        }
        catch (Exception e)
        {
            _logger.LogError(e.Message);
            return Task.CompletedTask;
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        var stopWatch = Stopwatch.StartNew();
        await base.StopAsync(cancellationToken);
        // it will print 30 seconds if stopped with ctrl + c
        _logger.LogInformation($"Worker Service Stopped in : {stopWatch.ElapsedMilliseconds}");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        stoppingToken.Register(() => _logger.LogInformation($"Worker service token is canceled"));

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWork();
            }
            catch (Exception e)
            {
                _logger.LogCritical(e, "I can not work anymore!");
                /* Trigger graceful shutdown of service
                 This will respect time set on host.
                 services.Configure<HostOptions>(options =>
                    {
                        options.ShutdownTimeout = TimeSpan.FromSeconds(30);
                    });
                 */
            }
            finally
            {
                // if you can not recover, stop it.
                // All hosted services in host will be stopped in reverse order of registration
                _hostApplicationLifetime.StopApplication();
            }
        }
    }

    private async Task<bool> DoWork()
    {
        _logger.LogInformation("I started doing work!");
        // press ctrl + c - after above message - ctrl + c is equal to StopService from Windows Host
        // defualt graceful shutdown is 6 seconds
        // work will never complete
        await Task.Delay(50000);
        _logger.LogInformation("I am done with work.");
        return true;
    }
}
Link to this note
public interface IAwsService{};
public interface IFileReader{};
public record CloudFileInfo(string SourcePath, string DestinationPath, string CloudTier);
public abstract class CloudFileMigration<T> //Its abstract thus can not be used directly, thus must be inherited.
{
    private readonly IAwsService _awsService;
    private readonly IFileReader _fileReader;

    // All subclasses will have to provide these services or dependencies
    protected CloudFileMigration(IAwsService awsService, IFileReader fileReader)
    {
        _awsService = awsService;
        _fileReader = fileReader;
    }
    // A Template Method  or Invariant Trait of the Pattern
    public async Task Migrate()
    {
        var items = await GetItems();
        await Task.WhenAll(items.Select(MigrateItem));
        async Task MigrateItem(T item)
        {
            var cloudFileInfo = await GetCloudFileInfo(item);
            await UploadToAws3(cloudFileInfo);
            var hasUpdateItemSucceeded = await UpdateItem(item, cloudFileInfo);
            if (!hasUpdateItemSucceeded)
                //handle failure
                await DeleteFromAws3(cloudFileInfo);
        }
    }
    private Task UploadToAws3(CloudFileInfo cloudFileInfo) => Task.CompletedTask; //Do the work
    private Task DeleteFromAws3(CloudFileInfo cloudFileInfo) => Task.CompletedTask; // Do the work

    /*
     * All abstract methods below will have different behavior in sub classes.
     * This is Variant Trait of the Pattern
     */
    protected abstract Task<IEnumerable<T>> GetItems();
    protected abstract Task<bool> UpdateItem(T item, CloudFileInfo cloudFileInfo);
    protected abstract Task<CloudFileInfo> GetCloudFileInfo(T item);

}
public record AssetFile(int PrimaryKey, string LocalPath, DateOnly CreatedDate);

public class AssetFileMigration : CloudFileMigration<AssetFile>
{
    public AssetFileMigration(IAwsService awsService, IFileReader fileReader) : base(awsService, fileReader){}
    protected override Task<IEnumerable<AssetFile>> GetItems() =>
        Task.FromResult(Enumerable.Empty<AssetFile>()); // Fetch it from source, say DB

    protected override Task<bool> UpdateItem(AssetFile item, CloudFileInfo cloudFileInfo) =>
        Task.FromResult(true); //Update DB & log etc

    // 1- Store in bucket A, Cloud Tier, And Local Path To Read from
    protected override Task<CloudFileInfo> GetCloudFileInfo(AssetFile item) =>
        Task.FromResult(new CloudFileInfo("", "", ""));
}
public record StatementFile(int PrimaryKey, string LocalPath, DateOnly LastUpdatedDate);
public class StatementFileMigration : CloudFileMigration<StatementFile>
{
    public StatementFileMigration(IAwsService awsService, IFileReader fileReader) : base(awsService, fileReader){}

    protected override Task<IEnumerable<StatementFile>> GetItems() =>
        Task.FromResult(Enumerable.Empty<StatementFile>()); // Fetch it DB

    protected override Task<bool> UpdateItem(StatementFile item, CloudFileInfo cloudFileInfo) =>
        Task.FromResult(true); //Update DB

    // 1- Store in bucket B, Cloud Tier based on LastUpdateDate, And Local Path To Read from
    protected override Task<CloudFileInfo> GetCloudFileInfo(StatementFile item) =>
        Task.FromResult(new CloudFileInfo("", "", ""));
}
Link to this note
public interface IAwsService{};
public interface IFileReader{};
public record CloudFileInfo(string SourcePath, string DestinationPath, string CloudTier);
public record AssetFile(int PrimaryKey, string LocalPath, DateOnly CreatedDate);
public record StatementFile(int PrimaryKey, string LocalPath, DateOnly LastUpdatedDate);
public class CloudFileMigrationSimple
{
    private readonly IAwsService _awsService;
    private readonly IFileReader _fileReader;


    public CloudFileMigrationSimple(IAwsService awsService, IFileReader fileReader)
    {
        _awsService = awsService;
        _fileReader = fileReader;
    }

    // A Template Method  or Invariant Trait of the Pattern
    // Steps getItems, UploadToAws3, updateItem are executed in same order, and handling of failure
    // While Func<Task<IEnumerable<T>>> getItems, Func<T, Task<bool>> updateItem behavior depends upon caller
    public async Task Migrate<T>(Func<Task<IEnumerable<T>>> getItems, CloudFileInfo cloudFileInfo,
        Func<T, Task<bool>> updateItem)
    {
        var items = await getItems();
        await Task.WhenAll(items.Select(MigrateItem));

        async Task MigrateItem(T item)
        {
            await UploadToAws3(cloudFileInfo);
            var hasUpdateItemSucceeded = await updateItem(item);
            if (!hasUpdateItemSucceeded)
                //handle failure
                await DeleteFromAws3(cloudFileInfo);
        }
    }

    private Task UploadToAws3(CloudFileInfo cloudFileInfo) => Task.CompletedTask; //Do the work
    private Task DeleteFromAws3(CloudFileInfo cloudFileInfo) => Task.CompletedTask; // Do the work
}

public static class Client
{
    public static async Task Run()
    {
        CloudFileMigrationSimple migrationSimple = new(default, default);
        await migrationSimple.Migrate(() => Task.FromResult(Enumerable.Empty<StatementFile>()), default,
            (item) => Task.FromResult(true));

        await migrationSimple.Migrate(() => Task.FromResult(Enumerable.Empty<AssetFile>()), default, UpdateAssetFile);

        Task<bool> UpdateAssetFile(AssetFile item)
        {
            return Task.FromResult<bool>(true);
        }
    }
}
Link to this note
C# / NOTE 04

Tuples Equality

var adnan = (Name: "Adnan", Age: 40);
var anotherAdnan = (Name: "Adnan", Age: 40);
var areTheySame = adnan == anotherAdnan ? "Yes" : "No";
Console.WriteLine($"Are they same? {areTheySame}");
//output: Are they same? Yes
var someoneElse = (FirstName: "Adnan", Age: 40);
areTheySame = adnan == someoneElse ? "Yes" : "No";
Console.WriteLine($"Are they same? {areTheySame}");
//output: Are they same? Yes
/*
 But why?
 Because Named Tuples are syntax sugar. It makes code more readable.
 Tuples equality is done on Tuple properties, Item1, Item2
*/
Link to this note
C# / NOTE 05

C# 9 Records

AnsiConsole.MarkupLine("[bold green]Hi, My name is record, I was born in C# 9.[/]");
var size = 10;
var names = new Name[size];

for (var i = 0; i < size; i++) names[i] = new Name(Faker.Name.Last(), Faker.Name.First());

try
{
    //to sort implement IComparable - seen below
    foreach (var name in names.OrderByDescending(x => x))
    {
        //create copied with with
        var anotherName = name with { };
        AnsiConsole.WriteLine(anotherName.ToString());//prints same value as name
        AnsiConsole.WriteLine(name.ToString());
    };

    //deconstruction
    foreach (var (lastName, firstName) in names)
    {
        AnsiConsole.WriteLine($"{firstName}, {lastName}");
    }
}
catch (Exception e)
{
    AnsiConsole.WriteLine("I fail to sort when IComparable is not implemented.");
    AnsiConsole.WriteException(e);
}

record Name(string LastName, string FirstName) : IComparable<Name>
{
    public int CompareTo(Name? other)
    {
        return other is null ? 1 : string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);
    }

    public override string ToString()
    {
        return $"{LastName}, {FirstName}";
    }
}

Link to this note