August 25, 2026

Library And Test Projects Generator

Here is a Linqpad file. You specify the root directory name and it will generate a directory structure with a solution file in the root directory. Under the root directory are 2 project directories, one a c# library project and the other is an NUnit test project for testing the library. The root directory is created in the Temp directory.

async void Main()
{
    IFileSystem fileSystem = new FileSystem();

    string linqPadFile = "";
    string projectName = "YYY";
    var testDirPath = Path.Combine(Path.GetTempPath(), projectName);
    var rootDir = fileSystem.Directory.CreateDirectory(testDirPath);
    Debug.Assert(rootDir.Exists, $"Directory {rootDir.FullName} not found.");

    var projFileCreator = new CSharpProjectFileCreator(fileSystem);
    var response = projFileCreator.Create(new CreateProjectRequest(rootDir.CreateSubdirectory($"{projectName}"), $"{projectName}.csproj", 
        ["Microsoft.Extensions.Logging", "System.IO.Abstractions", "Microsoft.Extensions.DependencyInjection"], $"{projectName}.cs", internalsVisibleTo: $"{projectName}.NUnit"));
    var response2 = projFileCreator.Create(new CreateProjectRequest(rootDir.CreateSubdirectory($"{projectName}.NUnit"), $"{projectName}.NUnit.csproj", 
        ["NUnit", "NUnit3TestAdapter", "Microsoft.NET.Test.Sdk", "System.IO.Abstractions", "System.IO.Abstractions.TestingHelpers", "coverlet.collector"],
        $"{projectName}.NUnit.cs", NUnitSamples.NUnitSampleTest));
    projFileCreator.CreateSolutionFile(testDirPath, projectName);


    // Output
    //Console.WriteLine($"Project File Name: {projectFileName}");
    //Console.WriteLine("");
    //Console.WriteLine("--- Project Contents ---");
    //Console.WriteLine(projectFileContents);
}

public record CreateProjectRequest(IDirectoryInfo TargetDir, string ProjectFileName, string[] NugetReferences, string ContentFileName = "", string Content = "", string internalsVisibleTo = "");
public record CreateProjectResponse(bool Successful, string msg = "");


public class CSharpProjectFileCreator
{
    private readonly IFileSystem _fileSystem;

    public CSharpProjectFileCreator(IFileSystem fileSystem)
    {
        _fileSystem = fileSystem;
    }

    public CreateProjectResponse Create(CreateProjectRequest request)
    {
        bool fullySuccessful = false;
        if (!request.TargetDir.Exists)
        {
            return new CreateProjectResponse(false, $"Directory {request.TargetDir.FullName} not found.");
        }

        var projectFilePath = Path.Combine(request.TargetDir.FullName, request.ProjectFileName);
        var projectFi = _fileSystem.FileInfo.New(projectFilePath);
        var successful = GenerateProjectFile(projectFi.FullName, request.NugetReferences, request.internalsVisibleTo);
        if (!successful)
        {
            return new CreateProjectResponse(false, $"Project file {projectFi.FullName} not created.");
        }

        var contentFilePath = Path.Combine(request.TargetDir.FullName, request.ContentFileName);
        var contentFi = _fileSystem.FileInfo.New(contentFilePath);
        successful = CreateFile(contentFi.FullName, request.Content);

        if (!successful)
        {
            return new CreateProjectResponse(false, $"Project file {projectFi.FullName} created, but not the content file {contentFi.FullName} .");
        }

        return new CreateProjectResponse(fullySuccessful, $"Project file {projectFi.FullName} and Content file {contentFi.FullName} were created");
    }


    private bool GenerateProjectFile(string projectFullName, string[] nugetReferences, string internalsVisibleTo = "")
    {
        string packageReferences = "";
        if (nugetReferences.Any())
        {
            packageReferences = string.Join(Environment.NewLine,
                nugetReferences.Select(x => $"    <PackageReference Include=\"{x}\" Version=\"*\" />"));
        }

        var projectContent = projectTemplate.Replace("{packageReferences}", packageReferences);
        string internalsVisibleToSection = "";
        if (internalsVisibleTo.Length > 0)
        {
            internalsVisibleToSection = internalsVisibleTemplate.Replace("{internalsVisibleTo}", internalsVisibleTo);
        }
        projectContent = projectContent.Replace("{internalsVisibleToSection}", internalsVisibleToSection);
        //_fileSystem.File.WriteAllText(projectFullName, projContent);
        return CreateFile(projectFullName, projectContent); //_fileSystem.File.Exists(projectFullName);
    }

    private bool CreateFile(string fileFullName, string content)
    {
        _fileSystem.File.WriteAllText(fileFullName, content);
        return _fileSystem.File.Exists(fileFullName);
    }

    public bool CreateSolutionFile(string rootDirectoryPath, string projectName)
    {
        var solnFilePath = Path.Combine(rootDirectoryPath, projectName + ".slnx");
        bool successful = CreateFile(solnFilePath, projectSolutionTemplate.Replace("{projectName}", projectName));

        return successful;
    }


    private static string projectTemplate =
        """
        <Project Sdk="Microsoft.NET.Sdk">
          <PropertyGroup>
            <OutputType>Exe</OutputType>
            <TargetFramework>net10.0</TargetFramework>
            <LangVersion>latest</LangVersion>
            <Nullable>enable</Nullable>
          </PropertyGroup>

          <!-- Project references go here-->
          <ItemGroup>
          {packageReferences}
          </ItemGroup>

          {internalsVisibleToSection}
        </Project>
        """;
        
    private static string  projectSolutionTemplate = 
        """
        <Solution>
          <Configurations>
            <Platform Name="Any CPU" />
          </Configurations>
          <Folder Name="/Solution Items/">
            <File Path=".editorconfig" />
          </Folder>
          <Project Path="{projectName}/{projectName}.csproj" />
          <Project Path="{projectName}.NUnit/{projectName}.NUnit.csproj" />
        </Solution>
        """;

    private static string internalsVisibleTemplate =
        """
        <ItemGroup>
            <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
                <_Parameter1>{internalsVisibleTo}</_Parameter1>
            </AssemblyAttribute>
        </ItemGroup>
        """;

}

public class NUnitSamples
{
   public static readonly string NUnitSampleTest = // See https://rbovill.blogspot.com/2006/01/using-nunit.html
    """
    // See https://rbovill.blogspot.com/2006/01/using-nunit.html
    using NUnit.Framework;

    [TestFixture]
    public class SomeTester
    {
      // Format of a Test method, Try to put all the setup for the test in the test.
      // If necessary add private Setup/Initialise/Teardown methods to assist this 
      // rather than using the one listed above
      [Test]
      public void SomeTest1()
      {
      }
     
      // You can add test parameters to a test and use it to test multiple cases 
      [Test]
      [TestCase(5.0d, 0.0d, 3.0d)]
      [TestCase(5.0d, 1.0d, 3.0d)]
      [TestCase(5.0d, 0.0d, 5.0d)]
      public void SomeTest2(double fullLengthSecs, double startTimeSecs, int expectedValue)
      {
      }
    }   
    """;
}

C# implicit and explicit operators

What they are (quick)

  • Implicit operator: defines an automatic conversion from one type to another that the compiler applies without a cast.
  • Explicit operator: defines a conversion that requires a cast and is used when the conversion may lose information or fail.

Syntax (C#)

Implicit:

public static implicit operator TargetType(SourceType s) {
    // create and return TargetType from s
}

Explicit:

public static explicit operator TargetType(SourceType s) {
    // create and return TargetType from s
}

Simple examples

Implicit (safe, no data loss):

struct Meters {
    public double Value;
    public static implicit operator double(Meters m) => m.Value;
    public static implicit operator Meters(double d) => new Meters { Value = d };
}

Meters m = new Meters { Value = 1.5 };
double d = m;        // implicit
Meters m2 = 2.0;     // implicit

Explicit (possible loss/failure):

struct ByteSized {
    public byte Value;
    public static explicit operator ByteSized(int i) {
        if (i < 0 || i > 255) throw new OverflowException();
        return new ByteSized { Value = (byte)i };
    }
}

int x = 300;
ByteSized b = (ByteSized)x; // requires cast; may throw an exception

When to use which

  • Use implicit when the conversion is:
    • Lossless (no precision or semantic loss).
    • Safe and not surprising to callers.
    • Cheap and well-defined in both directions (often).
  • Use explicit when:
    • The conversion can lose information (precision, range).
    • It can throw or fail.
    • It’s potentially surprising or semantically significant.
    • Converting from a wide to a narrower type (e.g., double → int).

Best practices

  • Prefer explicit for any conversion that can lose data or change meaning.
  • Keep conversions simple and obvious; avoid heavy logic or external dependencies.
  • Provide symmetric conversions when sensible (if you define implicit A→B, consider B→A if safe).
  • Document conversions clearly on the type.
  • Consider factory methods (FromX, ToX) when conversion is complex or may fail with domain-specific errors.
  • Avoid implicit conversions between unrelated types to prevent hidden bugs and API surprises.

Common pitfalls

  • Implicit conversions can make code less readable and introduce subtle bugs when multiple conversion paths exist.
  • Overusing implicit can cause ambiguous overload resolution.
  • Throwing exceptions inside conversion operators is allowed but make the operator explicit if exceptions are possible.

AI Generated

C# to Javascript Conversion Using AI

I asked AI (Code Haiku 4.5) to convert a C# class to Javascript. AI seems to excel at small conversions task like this. The class takes the lines of a SRT subtitle file and converts them to a VTT file. VTT has an optional additional title and note headers so I allows these to be set in the constructor. Note that it just deals with the file as a IEnumerable. Opening the file and streaming it to an instance of this class would be the resposibility of another class, which makes this very easy to test. It is also I think, a nice example of the power of an Iterator function/method and what can be achieved with them. Note that this class does not fix the Cue timings if they are out of order, etc.. Also the class could be tidied up quite a bit: _lineNumber is not required, the ParseState could be set in the Iterator. Well here is the C# version

public class SrtToVttConvertor
{
    private int _lineNumber = 0;
    private ParseState _parseState = ParseState.Starting;
    private readonly string _title = "";
    private readonly string _note = "";

    public SrtToVttConvertor(string title = "", string note = "")
    { 
        _title = title ?? "";
        _note = note ?? "";
    }

    private enum ParseState
    {
        None = 0,
        Starting = 1,
        SearchCue = 2,
        Subtitles = 3
    }


    public IEnumerable<string> Convert(IEnumerable<string> lines)
    {
        foreach (var line in lines)
        {
            _lineNumber++;
            string temp = "";

            switch (_parseState)
            {
                case ParseState.Starting: // Output the start of the WEBVTT file
                    {
                        temp = "WEBVTT " + _title;
                        yield return temp.TrimEnd();
                        yield return "";
                        if (!string.IsNullOrWhiteSpace(_note))
                        {
                            yield return "NOTE " + _note;
                            yield return "";
                        }
                        _parseState = ParseState.SearchCue;
                        break;
                    }

                case ParseState.SearchCue: // Seach for a cue (timings) line (ignore the cue number line)
                    if (line.Contains("-->"))
                    {
                        yield return line.Replace(',', '.').Trim(); // "00:04:08,624 --> 00:04:09,833" Becomes "00:04:08.624 --> 00:04:09.833"
                        _parseState = ParseState.Subtitles;
                    }
                    break;

                case ParseState.Subtitles: // After cue line add all non-blank lines until another blank line is reached
                    if (string.IsNullOrWhiteSpace(line))
                    {
                        yield return "";
                        _parseState = ParseState.SearchCue;
                    }
                    else
                    {
                        yield return line.Trim();
                    }
                    break;
            }
        }
    }
}

I was not even sure whether Javascript had Enumerators, it does they are called generators. Here is what it came up with:

class SrtToVttConvertor {
    #lineNumber;
    #parseState;
    #title;
    #note;

    static ParseState = {
        None: 0,
        Starting: 1,
        SearchCue: 2,
        Subtitles: 3
    };

    constructor(title = "", note = "") {
        this.#lineNumber = 0;
        this.#parseState = SrtToVttConvertor.ParseState.Starting;
        this.#title = title ?? "";
        this.#note = note ?? "";
    }

    *convert(lines) {
        for (const line of lines) {
            this.#lineNumber++;
            let temp = "";

            switch (this.#parseState) {
                case SrtToVttConvertor.ParseState.Starting:
                    temp = "WEBVTT " + this.#title;
                    yield temp.trimEnd();
                    yield "";
                    if (this.#note.trim().length > 0) {
                        yield "NOTE " + this.#note;
                        yield "";
                    }
                    this.#parseState = SrtToVttConvertor.ParseState.SearchCue;
                    break;

                case SrtToVttConvertor.ParseState.SearchCue:
                    if (line.includes("-->")) {
                        yield line.replaceAll(',', '.').trim();
                        this.#parseState = SrtToVttConvertor.ParseState.Subtitles;
                    }
                    break;

                case SrtToVttConvertor.ParseState.Subtitles:
                    if (line.trim().length === 0) {
                        yield "";
                        this.#parseState = SrtToVttConvertor.ParseState.SearchCue;
                    } else {
                        yield line.trim();
                    }
                    break;
            }
        }
    }
}

// Usage:
const converter = new SrtToVttConvertor("My Video", "Subtitle note");
const vttLines = [...converter.convert(srtLines)];

August 24, 2026

Host Application Lifetime Events

Sometimes it is useful to know when the host web application has started listening for calls to web site pages and also when it is stopping listening and completely stopped listening. You can register for these events in .NET applications. In my case my web application needed to start a fileWatcher at the start and then stop it when the application had stopped.

/// <summary>
/// This class tells you when the Web Application 
/// Starts (just prior to listening for web requests) 
/// and Stops and has Stopped, so you can start things up 
/// and shut them down at the end if that kind of pattern/behaviour is required in your application.
/// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.0#ihostapplicationlifetime
/// </summary>
internal class WebApplicationLifetimeEvents : IHostedService
{
    private readonly ILogger _logger;
    private readonly IHostApplicationLifetime _appLifetime;
    private readonly IWebHostEnvironment _webhostEnvironment;


    public WebApplicationLifetimeEvents(
        ILogger<WebApplicationLifetimeEvents> logger,
        IHostApplicationLifetime appLifetime,
        IWebHostEnvironment webhostEnvironment
        )
    {
        _logger = logger;
        _appLifetime = appLifetime;
        _webhostEnvironment = webhostEnvironment;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _appLifetime.ApplicationStarted.Register(OnStarted);
        _appLifetime.ApplicationStopping.Register(OnStopping);
        _appLifetime.ApplicationStopped.Register(OnStopped);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("OnStarted has been called.");
        // Perform post-startup activities here:
        try
        {

        }
        catch (Exception ex)
        {
            _logger.LogError($"Exception in OnStarted {ex}");
            throw;
        }
    }

    private void OnStopping()
    {
        _logger.LogInformation("OnStopping has been called.");

        // Perform on-stopping activities here
    }

    private void OnStopped()
    {
        _logger.LogInformation("OnStopped has been called.");

        // Perform post-stopped activities here
    }
}

Usage, simply inject it to your dependency injection services


public static class DependencyInjection
{
    public static IServiceCollection ConfigureServices(this IServiceCollection services)
    {
        // Add application services.
        services.AddSingleton<IRsgConfiguration, RsgConfiguration>();
        services.AddSingleton<IHostedService, WebApplicationLifetimeEvents>();
        ...

IWebHostEnvironment MapPath Extensions

Extends the IWebHostEnvironment so that it is easy to find files in the wwwroot directory or in the in the content directory. Note that you can inject IWebHostEnvironment into a controller

public static class IWebHostEnvironmentExtender
{
    /// <summary>
    /// Maps a virtual path that begins with '~' to its corresponding physical path within the web root (wwwroot) directory.
    /// </summary>
    /// <remarks>Use this method to resolve virtual paths in web applications when accessing files or
    /// directories under the web root. If the input path does not start with '~', it is returned unchanged.</remarks>
    /// <param name="whe">The <see cref="IWebHostEnvironment"/> instance that provides information about the web hosting environment,
    /// including the web root path.</param>
    /// <param name="path">The virtual path to map. If the path starts with '~', it will be replaced with the web root path.</param>
    /// <returns>The physical path corresponding to the provided virtual path if it starts with '~'; otherwise, returns the
    /// original path.</returns>
    public static string MapWwwRootPath(this IWebHostEnvironment whe, string path)
    {
        return MapToRootPath(whe.WebRootPath.AsSpan(), path.AsSpan());

    }

    /// <summary>
    /// Maps a virtual path that begins with '~' to the physical content root path of the specified web host
    /// environment.
    /// </summary>
    /// <remarks>Use this method to resolve application-relative paths in scenarios where the content root
    /// path may vary, such as in different hosting environments.</remarks>
    /// <param name="whe">The web host environment that provides the content root path used for mapping.</param>
    /// <param name="path">The virtual path to map. If the path starts with '~', it is resolved relative to the content root path.</param>
    /// <returns>The physical path corresponding to the provided virtual path, or the original path if it does not start with
    /// '~'.</returns>
    public static string MapContentRootPath(this IWebHostEnvironment whe, string path)
    {
        return MapToRootPath(whe.ContentRootPath.AsSpan(), path.AsSpan());
    }

    internal static string MapToRootPath(
        ReadOnlySpan<char> rootPath,
        ReadOnlySpan<char> path)
    {
        var pathSafe = path.Trim();
        if (pathSafe.IsEmpty)
            return path.ToString();

        var ix = pathSafe.IndexOf('~');
        var restOfPathSafe = pathSafe[1..];
        var res = (ix == 0) ? string.Concat(rootPath, restOfPathSafe) : path.ToString();

        return res;
    }

}

Example Usage


// I had an "app_data" directory under my project for data files application data like the old .NET MVC
// This code would allow me to access it:
dirPath = webHostEnv.MapContentRootPath(Path.Combine("~", "app_data", "clipboard"));
// Sometimes I may want to access static resources in the "wwwroot" directory
_mainBookmarksJsonFile = environment.MapWwwRootPath(configuration.BookmarksJsonFile);

Simple FakeLogger

Use this fake logger when required within unit tests, where you are using Microsoft Logging and directly injecting the logger

public class FakeLogger<T> : ILogger<T>
{
    private sealed class NoOpDisposable : IDisposable
    {
        public void Dispose()
        {
        }
    }

    public IDisposable BeginScope<TState>(TState state) where TState : notnull
    {
        return new NoOpDisposable();
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception?, string> formatter)
    {
        string message = "";
        if (formatter != null)
        {
            message += formatter(state, exception);
        }
        var logLeveChar = logLevel.ToString()[0];
        Console.WriteLine($"{logLeveChar} - {typeof(T)} - {message}");
    }
}

Fake Logger Factory and Fake Loggers

When using Microsoft Logging this code allows you to create a FakeLoggerFactory that creates named FakeLoggers that all log to one Combined logger. This is useful for unit testing.


// You can define other methods, fields, classes and namespaces here
// What does this do? https://www.tutorialsteacher.com/core/fundamentals-of-logging-in-dotnet-core
/// <summary>
/// FakeLoggerFactory used in Unit testing. It creates loggers that log to a <see cref="CombinedLogging"/> object
/// </summary>
/// <inheritdoc />
public sealed class FakeLoggerFactory : ILoggerFactory
{
    readonly CombinedLogging _combinedLogging = new();

    /// <summary>
    /// Creates a new <see cref="FakeLoggerFactory"/> instance.
    /// </summary>
    public FakeLoggerFactory() { }

    /// <inheritdoc />
    /// <remarks>
    /// This returns a <see cref="FakeLogger2"/> instance of the given category.
    /// </remarks>
    public ILogger CreateLogger(string categoryName)
    {
        string tmp = $"CreateLogger {categoryName}";
        Console.WriteLine(tmp);
        return new FakeLogger2(categoryName, _combinedLogging);
    }

    /// <inheritdoc />
    public void AddProvider(ILoggerProvider provider)
    {
    }

    /// <inheritdoc />
    public void Dispose()
    {
    }

    /// <summary>
    /// Enumerate the combined log entries
    /// </summary>
    /// <returns></returns>
    public IEnumerable<LogEntry> Logs()
    {
        return _combinedLogging.Logs();
    }
}

[ExcludeFromCodeCoverage]
public class FakeLogger2 : ILogger
{
    private CombinedLogging _combinedLogging;
    private readonly string _category;

    /// <summary>
    /// Create a Fake Logger
    /// </summary>
    /// <param name="category">Specifying the category of the logging</param>
    /// <param name="combinedLogging">Combines the logging from all FakeLogger2s</param>
    public FakeLogger2(string category, CombinedLogging combinedLogging)
    {
        _category = category;
        _combinedLogging = combinedLogging;
    }

    public IDisposable? BeginScope<TState>(TState state) where TState : notnull
    {
        return new NoOpDisposable();
    }

    private sealed class NoOpDisposable : IDisposable
    {
        public void Dispose()
        {
        }
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        _combinedLogging.Log(_category, logLevel, eventId, state, exception, formatter);
    }
}

/// <summary>
/// To record a LogEntry
/// </summary>
/// <param name="Level">The LogLevel of the entry</param>
/// <param name="Category">The category of the entry, so an ILogger<T> will
/// log to a category of T where T is the type as a string</param>
/// <param name="Text">Logged text</param>
[ExcludeFromCodeCoverage]
public sealed record LogEntry(LogLevel Level, string Category, string Text)
{
    /// <summary>
    /// For Logger logging type T which has as a fully qualified type name of X.Y.Z, 
    /// ie. ILogger<T> we usually are only interested in the last part (class name), "Z"
    /// </summary>
    /// <returns>Class name part of the Type</returns>
    public string MinimalCategory()
    {
        Debug.Assert(Category is not null, $"Field {nameof(Category)} is null");

        var span = Category.AsSpan();
        int ix = span.LastIndexOf('.'); // Look for last period/full stop character
        if (ix > 0)
        {
            span = span[ix..];
        }
        return span.ToString();
    }

    public override string ToString()
    {
        return $"{DateTime.UtcNow.ToRoundTripFormatString()} - {Level.ToString()[0..3].ToUpperInvariant()} - {MinimalCategory()} - {Text}";
    }
}

/// <summary>
/// Used to combine the logging of all FakeLogger2 loggers
/// </summary>
[ExcludeFromCodeCoverage]
public class CombinedLogging
{
    /// <summary>
    /// Store the logged lines by logging level here in logged order
    /// </summary>
    private readonly List<LogEntry> _allLogs = new();

    public CombinedLogging()
    {
    }

    public void Log<TState>(string category, LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        string message = "";
        if (formatter != null)
        {
            message += formatter(state, exception);
        }
        var logLeveChar = logLevel.ToString()[0];
        Console.WriteLine($"{logLeveChar} - {category} - {message}");
        _allLogs.Add(new LogEntry(logLevel, category, $"{message}"));
    }

    /// <summary>
    /// Retrieve All Logging
    /// </summary>
    /// <returns>All logging</returns>
    public IEnumerable<LogEntry> Logs()
    {
        foreach (var line in _allLogs)
        {
            yield return line;
        }
    }
}