August 15, 2023

Simple Line Editor

Here is the LineEditor, it can work on a file or a string:

// Here is the definition of a line edit
public interface ILineEdit
{
    string ApplyEdit(string input);
}

/// <summary>
/// Perform a bunch of edits on a text file or string on a line by line basis. 
/// Only search and replace type edits are available.
/// </summary>
public class TextLineEditor
{
    private List<ILineEdit> _pendingEdits = new List<ILineEdit>();

    public TextLineEditor()
    {
    }

    public void ApplyEditsToFile(string inputPath, string outputPath)
    {
        Debug.Assert(!string.IsNullOrWhiteSpace(inputPath));
        Debug.Assert(!string.IsNullOrWhiteSpace(outputPath));
        Debug.Assert(!outputPath.Equals(inputPath)); 
        var inputFileInfo = new FileInfo(inputPath);
        var outputFileInfo = new FileInfo(outputPath);
        Debug.Assert(inputFileInfo.Exists);
        Debug.Assert(!outputFileInfo.Exists);

        using (TextWriter writer = new StreamWriter(outputFileInfo.FullName))
        {
            using (TextReader reader = new StreamReader(inputFileInfo.FullName))
            {
                ApplyEdits(writer, reader);
            }
        }
    }

    public string ApplyEditsToString(string text)
    {
        Trace.Assert(text != null);
        StringBuilder sb = new StringBuilder();
        using (TextWriter writer = new StringWriter(sb))
        {
            using (TextReader reader = new StringReader(text))
            {
                ApplyEdits(writer, reader);
            }
        }
        return sb.ToString();
    }

    private void ApplyEdits(TextWriter writer, TextReader reader)
    {
        string line = null;
        while ((line = reader.ReadLine()) != null)
        {
            foreach (ILineEdit edit in _pendingEdits)
            {
                line = edit.ApplyEdit(line);
            }
            if (line != null)
                writer.WriteLine(line);
        }
    }

    public void AddEdit(ILineEdit edit)
    {
        Trace.Assert(edit != null);
        _pendingEdits.Add(edit);
    }
}

This TextLineEditor could be replaced to work with IEnumerab<string> for line input and returning a IEnumerable<string> as an output. Here is a LineEdit example. Note that it can do deletes by replacing with a blank string.

public class SearchAndReplace : ILineEdit
{
    public enum RegExUsage
    {
        None = 0,
        Use
    }

    public SearchAndReplace()
    {
    }

    public SearchAndReplace(string searchFor, string replaceWith, bool extendedChars = false, RegExUsage regExUse = RegExUsage.None)
    {
        if (string.IsNullOrEmpty(searchFor))
            throw new ArgumentException("Cannot be null or an empty string", "searchFor");
        if (replaceWith == null)
            throw new ArgumentException("Cannot be null", "replaceWith");
        this.SearchFor = searchFor;
        this.ReplaceWith = replaceWith;
        this.ExtendedChars = extendedChars;
    }

    public bool IsRegEx { get; set; } = false;
    public string SearchFor { get; set; } = "";
    public string ReplaceWith { get; set; } = "";
    public bool ExtendedChars { get; set; } = false;

    public string ApplyEdit(string input)
    {
        if (ExtendedChars)
        {
            SearchFor = SearchFor.Replace("\\t","\t").Replace("\\n","\n").Replace("\\r","\r");
        }
        return (IsRegEx) ? SearchReplaceInString(input) : SearchReplaceInStringRegEx(input);
    }

    private string SearchReplaceInString(string input)
    {
        string output = input.Replace(SearchFor, ReplaceWith);
        return output;
    }

    private string SearchReplaceInStringRegEx(string input)
    {
        string output = Regex.Replace(input, SearchFor, ReplaceWith);
        return output;
    }
}

Here is an example usage:
{
	var inputResFilePath = ...
	var outputResNewCsvFilePath ...
	var outputResCsvFilePath = ...

	var inputCnbFilePath = ...
	var outputCnbNewCsvFilePath ...
	var outputCnbCsvFilePath = ...


	var replaceCommentChars = new SearchAndReplace("#", "*");
    {
        var replaceTabsWithGT = new SearchAndReplace("\t", " > ");

        TextLineEditor resEditor = new();
        resEditor.AddEdit(replaceTabsWithGT);
        resEditor.AddEdit(replaceCommentChars);
        
        resEditor.ApplyEditsToFile(inputResFilePath, outputResNewCsvFilePath);

        File.Delete(outputResCsvFilePath);
        File.Move(outputResNewCsvFilePath, outputResCsvFilePath);
    }
    {
        var replaceExclamationChars = new SearchAndReplace("!", "*");
        var replaceTabWithHyphen = new SearchAndReplace("\t", " - ");

        TextLineEditor cnbEditor = new();
        cnbEditor.AddEdit(replaceTabWithHyphen);
        cnbEditor.AddEdit(replaceCommentChars);
        cnbEditor.AddEdit(replaceExclamationChars);

        cnbEditor.ApplyEditsToFile(inputCnbFilePath, outputCnbNewCsvFilePath);

        File.Delete(outputCnbCsvFilePath);
        File.Move(outputCnbNewCsvFilePath, outputCnbCsvFilePath);
    }
}

July 21, 2023

Startup For Dependency Injection, Settings and Logging

The Startup format for a Console program with a Serilog logger

You need quite a few packages:
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.TraceSource" Version="7.0.0" />
    <PackageReference Include="Serilog" Version="2.12.0" />
    <PackageReference Include="Serilog.Enrichers.Environment" Version="2.2.0" />
    <PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
    <PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
    <PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
    <PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
    <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
    <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
    <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

Here is a Startup class:

using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Serilog;
using Serilog.Core;
using Serilog.Formatting.Compact;


public class Startup
{
  IConfigurationRoot? Configuration { get; init; }
 
  public AppSettings AppSettings { get; private set; } = new AppSettings();
 
  public Startup()
  {
     Configuration = BuildConfiguration();
  }
 
  public IServiceCollection ConfigureServices(string loggingFilePath)
  {
    IServiceCollection services = new ServiceCollection();
    services.AddLogging(builder => builder.AddSerilog(
        new LoggerConfiguration()
#if DEBUG
        .MinimumLevel.Debug() // Log Debug level and higher
#else
        .MinimumLevel.Information() // Log Information level and higher
#endif
        // To read the settings from the configuration file:
        //.ReadFrom.Configuration(this.Configuration!)
       .WriteTo.Console() // To make the console output window a logger sink
       // To write to a custom logging file and render object properties in JSon format
       .WriteTo.File(new RenderedCompactJsonFormatter(), loggingFilePath)
       .CreateLogger()));
 
    RegisterDependencyInjectedServices(services);
    return services;
  }
 
  private static IServiceCollection RegisterDependencyInjectedServices(IServiceCollection services)
  {
    // Use this format: services.AddScoped/Transient/Singleton<ISomething, Something>();
    // For example
    services.AddSingleton<IResDataRepo, ResDataRepo>();
    services.AddSingleton<ICnbDataRepo, CnbDataRepo>();
    services.AddTransient<IGuiInputListener, GuiListener>();    
    return services;
  }

  private IConfigurationRoot BuildConfiguration()
  {
     // To create a custom AppSetting file:
     const string settingsFileName = "{MyApplicationName}.AppSettings.json";
     string currentDirectory = Directory.GetCurrentDirectory();
     var builder = new ConfigurationBuilder()
          .SetBasePath(currentDirectory)
          .AddJsonFile(settingsFileName, optional: false);
     var config = builder.Build();
 
     var settings = config.GetSection("AppSettings").Get<AppSettings>();
     if (settings == null)
     {
         Console.WriteLine($"ERR: {settingsFileName} not found in current directory {currentDirectory}, using defaults!");
     }
     AppSettings = settings ?? new AppSettings();
     return config;
  }
}

February 13, 2023

File Renamer

Rename a file when you have a path. This was written as a LinqPad script

void Main()
{
    Console.WriteLine(FileRenamer. CalcNewFilePath (@"c:/some/Directory/File.pdf"));
    Console.WriteLine(FileRenamer. CalcNewFilePath (@"File.pdf"));
    Console.WriteLine(FileRenamer. CalcNewFilePath (@"C:File.pdf"));
    Console.WriteLine(FileRenamer. CalcNewFilePath (@"/some/Directory/File.pdf"));
    Console.WriteLine(FileRenamer. CalcNewFilePath (@"/some\Directory/File.pdf"));
    Console.WriteLine(FileRenamer. CalcNewFilePath (@"e:\zdump\some\Directory\File.pdf"));
}

public static class FileRenamer
{
    const string Suffix = @"-xx";

    /// <summary>
    /// Calculate the file path of the file, changing the file name
    /// in the process
    /// </summary>
    /// <param name="filePath"></param>
    /// <returns></returns>
    public static string CalcNewFilePath(string filePath)
    {
        if (string.IsNullOrWhiteSpace(filePath))
            return "";
        var ext = Path.GetExtension(filePath);
        var bareFileName = Path.GetFileNameWithoutExtension(filePath);
       
        var path = GetFilePathStem(filePath);

    var fileNameTemplate = $"{path}{bareFileName}{Suffix}{ext}";
    return fileNameTemplate;
}

/// <summary>
/// Get the stem of the file path, everything before the actual filename.
/// It assumes that the last part of the path after the 
/// final Directory Seperator character is the filename
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static string GetFilePathStem(string filePath)
{
    var path = "";
    var endofPath1 = filePath.LastIndexOf(Path.DirectorySeparatorChar);
    var endofPath2 = Path.AltDirectorySeparatorChar != 
      Path.DirectorySeparatorChar ? filePath.LastIndexOf(Path.AltDirectorySeparatorChar) : -1;
    var endofPath = Math.Max(endofPath1, endofPath2);
    if (endofPath > 0)
    {
        path = filePath.Substring(0, endofPath + 1);
    }
    else // When the root does not use a directory separator char eg. "c:filename.pdf"
    {
        var root = Path.GetPathRoot(filePath);
        path = root;
    }
    return path;
}


 

January 8, 2023

Powershell AfterLogin & CheckIpAddressChange

AfterLogin.ps1
#$host.UI.RawUI.ForegroundColor = "Green"
Write-Host "*** After Login Prepartion ***"  -Foregroundcolor Green
$host.UI.RawUI.WindowTitle =  "*** After Login Prepartion ***"
Set-Variable -Name "AzureStorageEmulatorExe" -Value "C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\AzureStorageEmulator.exe"
Write-Output "*** Start Azure Storage Emulator ***"
& $AzureStorageEmulatorExe start
#Write-Host "*** LoggING into Azure ***"  -Foregroundcolor Green
#& az login --tenant 12345567-8910-47ff-0a65-2765a99ebbce
#Write-Host "*** LoggED into Azure ***"  -Foregroundcolor Green
& $AzureStorageEmulatorExe status
& "C:\Users\...\Documents\BatchFiles\CheckIpAddressChange.ps1"
Pause
CheckIpAddressChange.ps1
$currIpAddress = (Invoke-WebRequest -uri https://ifconfig.me/ip).Content
$prevIpAddress = "155.15.12.31"
$arkpdFirewallAccess = https://.../resource/subscriptions/resourceGroups/networking

#$host.UI.RawUI.ForegroundColor = "Green"
Write-Output "*** Checking for IP Address Change ***"  -Foregroundcolor Green
Write-Output "My current  IP address: $($currIpAddress)"
Write-Output "My previous IP address: $($prevIpAddress)"
$lastDifferentIpAddress = "15.17.107.14"
Write-Output "My last different IP address: $($lastDifferentIpAddress)"
Write-Output "if it changes, re-run ~\Scripts\IpRestrictionTool\allow-my-ip.ps1"
Write-Output "Also navigate to '$($arkpdFirewallAccess)' to update the firewall rule"

Powershell - Find My Ip Address

Clear
$currIpAddress = (Invoke-WebRequest -uri https://ifconfig.me/ip).Content
$prevIpAddress = "155.171.12.2"
Write-Output ""
Write-Output "My current  IP address: $($currIpAddress)"
Write-Output "My previous IP address: $($prevIpAddress)"
Write-Output "if it changes, re-run ~\Scripts\IpRestrictionTool\allow-my-ip.ps1"

December 11, 2022

Json Parsing Tests - ExpandoObject or Json strings

Setting Up Jason Parsing Tests Using Dynamic Objects Or Strings

[Test]
public void SomeTestUsingExpandoObject()
{
  // arrange
  const string getItemEventDataValue = @"{
      ""_PrimaryId"": ""F:\\dasda\\ToMO-005775.json"",
      ""_SecondaryId"": ""123456"",
      ""RetrievalResult"": ""Processed Successfully""
    }";
  dynamic indexItemRaw = new ExpandoObject();
  indexItemRaw.id = sourceIndexItemId;
  indexItemRaw.version = sourceIndexItemVersion.ToString(CultureInfo.InvariantCulture);
  dynamic source = new ExpandoObject();
  source.getItemEventData = getItemEventDataValue;
  indexItemRaw.source = source;

  // act
  IEnumerable<ClaimDto> claimsEnum = someMapper.Map(JObject.FromObject(indexItemRaw));
  var claimDtos = claimsEnum.ToList();
  
  //assert
  ...
}

[Test]
public void SomeTestUsingJSonStringOnly()
{
  //How to set up unit tests using strings to set up the data, instead of dynamic objects.
  //Sometimes it can be simpler, especially if you retrieve the raw data from a run
  //Here is the above test, arranged using a string as an example
  //In this case I thought it was more complicated to set everything up as a string
  //(because one of the json string object entries (the "getItemEventData") was itself a json string)
  // arrange
  string rawJToken = @"
  {
    ""id"": ""{sourceIndexItemId}"",
    ""version"": ""1.0"",
    ""source"": {
      ""getItemEventData"": ""{
         \""PrimaryId\"": \""F:\\\\dasda\\\\ToMO-005775.json\"",
         \""SecondaryId\"": \""123456\""
         \""RetrievalResult\"": \""Processed Successfully\""
      }""
    }
  }";
  rawJToken = rawJToken.Replace("{sourceIndexItemId}", sourceIndexItemId);
  JToken jtoken = JToken.Parse(rawJToken);

  //act
  IEnumerable<ClaimDto> claimsEnum = someMapper.Map(jtoken);
  var claimDtos = claimsEnum.ToList();

  //assert
  ...
}

December 4, 2022

Plant UML

Quite often I need to examine some existing code to find a solution to a problem. Sometimes this requires going in detail about what calls what and when. However, usually the code your looking at has many calls to many other objects. We don't want the details of every single call, just the important ones. Here sequence diagrams are very useful to document this visually. I prefer to use PlantUML for this because it allows you to write the diagram as text, PlantUml takes care of spacing everything appropriately. You can just get your object name/type and the method called on it from your IDE.
PlantUML is found at this web address: https://www.plantuml.com/ .
Here is some good documentation on how to use it: https://crashedmind.github.io/PlantUMLHitchhikersGuide/ .
Here is an example of using Plant UML to create a sequence diagram in UML:

Here is the resulting image, click on it to open it full size: