Seasons of Love - Ode to C#

C# Advent 2023 - Welcome!

This post is part of the C# Advent 2023 blog series. Check out all the other great posts here: csadvent.christmas

C# - I really miss it!

As I thought about my relationship to C#, its role in my skill set, and the value it has added to me throughout my programming career, I was struck by the realization that I really don’t write that much C# these days!

Shocking I know!

My transition to more advanced leadership roles, exploration of other skills like Angular, React, AWS, Python, and DevOps-enabling tools–all of this introduces the hard reality of opportunity cost. I can’t focus and expand on C# at the same time I’m tackling other domains of growth.

As with anything in life, as the Preacher in Ecclesiastes so aptly pointed out in Ecclesiastes 3:1-8, “There is an appointed time [season] for everything. And there is a time for every matter under heaven…”

Funny thing here is that in verse 5, there is even a time for embracing (or in this stretch of metaphor, em-bracing, err, um, you know–curly braces??) Anybody? […crickets…] Wow, tough crowd.

The point is, every skill we acquire in our career has seasons - the beginning where you don’t know what you don’t know and you have the beginner’s mind; the middle where you begin to understand the skill more fully and gain confidence and aptitude; the “apex” where you achieve maximum use of the skill; and finally, the end (or even “denouement”) which sees the use of that skill diminish, lessen, or just end entirely.

Yes, it is a bit sad, as Ecclesiastes tends to be, but recognizing the phases in the use of a skill makes a person appreciate it more when time is taken to reflect.

Believe it or not, I found C# confusing at the beginning, especially coming from a Visual Basic and VB.NET background.

Over time, I learned to love it, and then–LINQ!

My first C# opportunity… (x => “Please remove him”)!

My first consulting opportunity that involved C# (circa 2004) was an ASP.NET Web Forms application at a client where I backfilled for another consultant who was very comfortable using C#. I knew things about ASP.NET Web Forms using VB.NET, but C# was relatively new to me, I didn’t understand the differences in event binding syntax, and to top it all off, some recent medication changes had impacted my ability to stay awake. All of this led to poor performance on the assigned work, and the client asked that I be removed after 3 weeks! Wow, talk about a blow!

Fortunately, things improved immensely over time, and I found other opportunities to develop C# skills that were up to the requirements.

Generics, LINQ, and extension methods - Oh My!

I remember a colleague mentioning generics around 2005-2006. I had no idea what they were, but man, when I figured it out–my brain exploded on the wall! Mind blowing!

And then LINQ appeared on the scene in .NET 3.5 in a huge way. Thanks Anders and team!

Lambda expressions and extension methods took a while to understand, but in much the same way, they changed the way I think about code.

The End is Approaching…but when?

I realized recently that -

You’ll never be able to leverage every skill you’ve developed at the same time in a given job or assignment.

There will be times when you will set aside any given skill for a time. You may come back to it, or you may not.

But that’s OK, it’s about the journey, and the skills you build now inform the future endeavors.

Now, for the code!

So, without any further ado, here’s the program that I wrote to model my love for C#, how it ranks with being a Christian, a husband, a father, a family member, and friend. Oh, and bacon. Can’t forget that.

All so I can say, in code!:

MyLove loveOfCSharp = "CSharp";
var howDoILoveThee = loveOfCSharp.AllTheWays.Count();

The code uses many of my favorite C# features:

Read through the code, especially the unit tests, and you’ll see how I explore the relative ranking of my “declared” loves (which always looks great on paper, whether they actually play out that way in real life).

You’ll even see how I feel about the comparison of C# to Bacon. What do you think I like better?

Complete listing

(TL;DR here - dotnetfiddle

Please note: I wrote the entire program in Apple Notes, then expanded it using .NET fiddle, so if the xUnit.Net usage looks a little strange with the crude custom test runner code, it’s because I didn’t build it using an IDE. I still used the [Fact] attribute to mark my tests, but I’m not 100% sure the a dotnet test command would exhibit the same behavior.

Also, you may notice that I have a crude “Dump()” extension method. This comes from a similar but richer built-in method in one of my favorite tools, LINQPad by Joseph Albahari - thanks for everything you do!

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Xunit;
using Shouldly;
					
public class OdeToCSharp
{
    public static void Main() {
        MyLove loveOfCSharp = "CSharp";
        var howDoILoveThee = loveOfCSharp.AllTheWays.Count();
        Console.WriteLine($"{ nameof(howDoILoveThee).ToWords() } ({ loveOfCSharp })? Ways = { howDoILoveThee }");

		// Run all unit tests with a crude test runner
		var allTestsSucceeded = RunUnitTests<AdventTests>();
		
		Console.WriteLine(allTestsSucceeded ? "All tests passed!" : "Test failures occurred");
    }
	
	private static bool RunUnitTests<T>() where T: class, new()
	{
		bool overallSuccess = true;
		var tests = new T();
		var testMethods = tests
			.GetType()
			.GetMethods(BindingFlags.Public | BindingFlags.Instance)
			.Where(mi => mi.ReturnType == typeof(void))
			.Where(mi => mi.GetCustomAttributes<Xunit.FactAttribute>(inherit: false).Any())
			.ToArray();
		
		foreach(var testMethodInfo in testMethods)
		{
			var testMethodName = testMethodInfo.Name;
			Console.Write($"Executing test { testMethodName }...");
			try
			{
				testMethodInfo.Invoke(tests, new object[] {});
				
				Console.WriteLine("PASSED!");
			}
			catch(Exception ex)
			{
				overallSuccess = false;
				Console.WriteLine($"FAILED - {ex}");
			}
		}
		
		return overallSuccess;
	}
}

public class MyLove: IComparable<MyLove>
{
	private const string SIGN_OF_LOVE = "<3!";
    private const int MAX_LOVE = 1000;

    private enum MyDeclaredLoves: int
    {
        God = 0,
        Spouse = -1,
        Children = -2,
        RestOfFamily = - 3,
        Friends = - 4,
        CSharp = Friends - 1,
        Bacon = CSharp - 1,
        AllOtherThings = CSharp - 10
    }

    private string _thingILove;
    private int _howFarDownTheList;
	private MyDeclaredLoves _levelOfLove;

    public IEnumerable<string> AllTheWays => Enumerable.Repeat(SIGN_OF_LOVE, MAX_LOVE + _howFarDownTheList).ToArray();

    public IEnumerable<string> ThingsILoveMore => 
		typeof(MyDeclaredLoves)
		    .GetEnumValuesDictionary<MyDeclaredLoves>()
            .Where(kvp => ((int)kvp.Value) > _howFarDownTheList)
            .Select(kvp => $"{kvp.Key}")
			.ToArray();

    public MyLove(string thingILove)
    {
        _thingILove = thingILove;
        _howFarDownTheList = DetermineWhereOnTheList(_thingILove, out _levelOfLove);
    }

    private int DetermineWhereOnTheList(string thingIMightLove, out MyDeclaredLoves levelOfLove)
    {
        if(!MyDeclaredLoves.TryParse(thingIMightLove, out levelOfLove))
			levelOfLove = MyDeclaredLoves.AllOtherThings;
        return (int)levelOfLove;
    }

    public MyLove(object objectOfAffection): this($"{objectOfAffection}") {}

    public override string ToString() { return $"my love of {_thingILove}"; }

	int IComparable<MyLove>.CompareTo(MyLove otherLove) { return this._howFarDownTheList.CompareTo(otherLove._howFarDownTheList); }

    public static implicit operator MyLove(string thingILove) { return new MyLove(thingILove); }
	
	public static bool operator >(MyLove firstLove, MyLove secondLove)
	{
		return ((IComparable<MyLove>)firstLove).CompareTo(secondLove) > 0;
	}

	public static bool operator <(MyLove firstLove, MyLove secondLove)
	{
		return ((IComparable<MyLove>)firstLove).CompareTo(secondLove) < 0;
	}
	
	public static bool operator ==(MyLove firstLove, MyLove secondLove)
	{
		return ((IComparable<MyLove>)firstLove).CompareTo(secondLove) == 0;
	}
	
	public static bool operator !=(MyLove firstLove, MyLove secondLove)
	{
		return ((IComparable<MyLove>)firstLove).CompareTo(secondLove) != 0;
	}
	
	public override int GetHashCode() => _thingILove.GetHashCode();
	public override bool Equals(object o) { return this.GetHashCode() == o.GetHashCode(); }
	
	public static explicit operator int(MyLove love) => (int)love._levelOfLove;
}

public static class AdventExtensions
{
    public static string ToWords(this string instance) {
		// a crude word splitter using look behind and look ahead
        return string.Join(" ", Regex.Split(instance ?? "", @"(?<=[^A-Z]|I)(?=[A-Z])"));
    }

    public static IEnumerable<string> HowDoILoveThee(this string objectOfAffection)
    {
        MyLove waysILoveThee = objectOfAffection;
        var result = waysILoveThee.AllTheWays;
        return result;
    }
	
	public static IDictionary<string, object> GetEnumValuesDictionary<T>(this Type type) where T: System.Enum
	{
		return type
			.GetFields()
			.Where(f => f.IsLiteral)
			.ToDictionary(f => f.Name, f => f.GetRawConstantValue());
	}
	
	public static IEnumerable<T> Dump<T>(this IEnumerable<T> source, string title = null, bool includeTypeInfo = false)
	{
		// Note (JM, 12/07/2023): This method is inspired by LINQPad's super useful Dump() method that allows
		// outputting just about anything, then returns to "regularly-scheduled" programming
		if(!string.IsNullOrEmpty(title))
		{
			Console.WriteLine($"# { title }");
		}
		if(includeTypeInfo)
		{
			Console.WriteLine($"[{ typeof(T).FullName }]");
		}
		if(((object)source) == null)
		{
			Console.WriteLine("[null]");
			return source;
		}
		Type type = typeof(T);
		if(type.IsAssignableTo(typeof(IEnumerable)))
		{
			var items = ((IEnumerable)source).Cast<object>().Select(i=>$"{i}");
			Console.WriteLine("[" + string.Join("," + Environment.NewLine, items) + "]");
		}
		else
		{
			Console.WriteLine($"VALUE = {source}");
		}
		return source;
	}
}

public class AdventTests
{
	private const string YES_I_COULD_USE_A_CONSTANT_FOR_CSHARP = "CSharp";
	private const string BACON_IS_DEFINITELY_A_CONSTANT_DUH = "Bacon";
	
    [Fact]
    public void OdeToMyLove_WaysILoveCSharpIsNotZero()
    {
        var ways = "CSharp".HowDoILoveThee().Count();
        ways.ShouldBeGreaterThan(0);
    }

    [Fact]
    public void OdeToMyLove_LoveCSharpShouldExceed500()
    {
        var ways = "CSharp".HowDoILoveThee().Count();
        ways.ShouldBeGreaterThan(500);
    }

    [Fact]
    public void OdeToMyLove_WaysILoveCSharpExceedsWaysILoveBacon_ShockingIKnow()
    {
        var waysILoveCSharp = YES_I_COULD_USE_A_CONSTANT_FOR_CSHARP.HowDoILoveThee().Count();
        var waysILoveBacon = BACON_IS_DEFINITELY_A_CONSTANT_DUH.HowDoILoveThee().Count();
        waysILoveCSharp.ShouldBeGreaterThan(waysILoveBacon);
    }
  
    [Fact]
    public void OdeToMyLove_ILoveGodSpouseChildrenAndFriendsMoreThanBaconAndCSharpButItsClose()
    {
        var thingsILoveMore = new []{ "God", "Spouse", "Children", "Friends" };
        var nearTheTopButNotAsFar = new[]{ "Bacon", "CSharp" };

		var wrongPriorities = thingsILoveMore
			.Select(t => (MyLove)t)
			.SelectMany(l => l.ThingsILoveMore.Dump(title: $"Things I love more than {l}"))
			.Dump(title: "All the things I love more...")
			.Intersect(nearTheTopButNotAsFar.Dump(title: "Things near the top, but not as far ="))
			.ToArray()
			.Dump(title: "Intersection");
		
		wrongPriorities.ShouldBeEmpty();
    }
	
	[Fact]
	public void OdeToMyLove_ILoveCSharpMoreThanBacon()
	{
		MyLove loveOfCSharp = "CSharp";
		MyLove loveOfBacon = "Bacon";
		
		(loveOfCSharp > loveOfBacon).ShouldBeTrue();
	}
	
	[Fact]
	public void OdeToMyLove_ILoveOtherThingsAboutTheSame()
	{
		MyLove firstRandomLove = "IceCream";
		MyLove secondRandomLove = "VideoGames";
		
		(firstRandomLove == secondRandomLove).ShouldBeTrue();
	}
	
	[Fact]
	public void OdeToMyLove_ThingsILoveMoreThanGodShouldBeEmpty()
	{
		MyLove loveForGod = "God";
		loveForGod.ThingsILoveMore.Dump(title: "Things I love more than God").ShouldBeEmpty();
	}
	
	[Fact]
	public void ToWords_CamelCaseIdentifier_SplitsCorrectly()
	{
		string expectedValue = "expected Value";
		string howDoILoveThee = "how Do I Love Thee";
		
		(nameof(expectedValue)).ToWords().ShouldBe(expectedValue);
		
		(nameof(howDoILoveThee)).ToWords().ShouldBe(howDoILoveThee);
	}
}

Paraphrasing an oft-misattributed axiom (maybe you know exactly the original source)…

I would have written a shorter program if I had more time.

I’m sure there are opportunities for criticism and improvement, but this is what I could muster in the time I had available. If you have any comments, feedback or other fun things to say, you can find me here:

And finally, thank you!

Thanks for stopping by and looking at a snapshot of my love for C#, the seasons I find myself in, and my desire to touch this amazing and productive language more again in the future.

About the Author


Jeffrey A. Miller is a Senior Consultant, Trainer, Author, Speaker, and Leader. Jeffrey has over 25 years of experience helping organizations bring value to their mission through software. He has presented a variety of programming, data, and team topics at user groups and tech conferences.

Jeffrey and his wife, Brandy, are adoptive parents and have written a fun children’s book called “Skeeters” with proceeds supporting adoption.