Skip to main content

Keep it handy

👋🏽 Code Cards

Background or Worker Services in .NET

Used for long running tasks.

Background or Worker Services in .NET
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;
}
}

Template Method Pattern by using C# Inheritance

Template Method Pattern executes multiple & same steps in the same order and allows consumers to change the behavior of the steps. Below example, shows how you can migrate different types of files to AWS S3 from file share. This approach has advantage over delegate based approach when multiple consumers will be calling to migrate same file type(s) as call site will not need to pass the delegate.

Template Method Pattern by using C# Inheritance
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("", "", ""));
}

Template Method Pattern by using C# Delegates

Template Method Pattern executes multiple & same steps in the same order and allows consumers to change the behavior of the steps. Below example, shows how you can migrate different types of files to AWS S3 from file share.

Template Method Pattern by using C# Delegates
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);
}
}
}

Tuples Equality

Value Tuples support equality, and compared by using properties Item1, Item2 etc. Named tuples are syntax sugar.

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
*/

C# 9 Records

Hi, My name is record. I was born by C# 9. I support immutability and true value equality only when all members are of value types. I offer deconstruction by default and output nicely formatted string by default. I am best suited for DTO type objects.

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