Making C# more functional by adding a With method

Update (10th Nov ’20): C# 9 (released today with .NET5) includes built-in support for record types and the with keyword (see release notes)

Those of us who are primarily C# programmers, but are moving towards a more functional approach sometimes look at F# (and other FP languages of course) and wonder why we can’t have some of the nice features they offer. Prime amongst these are discriminated unions (which aren’t hard to generate for C#) and the with concept.

For those not familiar with this, one of the fundamental concepts in FP (and one that newcomers like myself found hard to work with initially) is that all values should be immutable, ie once created, you can’t change the data. That’s why they are called “values” and not “variables” as the data can’t vary once set.

For example, in C#, you can do the following…

int age = 42;
age = 43;

If age were immutable, you wouldn’t be able to do that. I’ll leave a discussion of why this is actually a good thing for those more knowledgeable than me, but I strongly recommend you read Functional Programming in C# by Enrico Buonanno, which has to be the most important C# book I’ve ever read.

The problem is, if the data structure is immutable, how do I change the data? Well, you don’t! What you do is create a new data structure that looks the same, but has different data in just the part you wanted to change. Again, if you’re struggling with this (nice theory, could never work in Real Life(TM), or “how on earth would I code that?”) read Enrico Buonanno’s book and all will become clear.

In F# this is easy. You can create a record type (think struct for simplicity) and then use the with keyword to create a copy with specific changes…

let g1 = {lat=1.1; long=2.2}
let g2 = {g1 with lat=99.9}   // create a new one

let p1 = {first="Alice"; last="Jones"}  
let p2 = {p1 with last="Smith"}

Example taken from Scott Wlaschin’s excellent web site.

So you can go and slap all your F# chums on the back and complement them on a great language feature. Then you can slink home and wonder why you can’t do anything like this in C#.

Well, with a bit of tinkering, you can. I can’t remember where I saw the inspiration for this idea, but you can write a With() method for any class that will allow you to simulate the with keyword. The naive approach would be to add a With() method for every property in the class, so a Person class would have a WithFirstName() method that creates a new Person with the same data as the current one, but an updated FirstName property. Similarly you could write a WithSurname() method and so on. You can see how this would get out of hand very quickly.

What would be better is an all-purpose With() method that allows you to specify any bits of the class you want. Named parameters allow us to do this easily…

Person jim = new Person("Jim", "Spriggs", 42); // data passed in the constructor
Person jimsTwin = jim.With(firstName: "Jemima"); // Jemima Spriggs is also 42
Person jimsOlderBrother = jim.With(firstName: "John", age: 44); // John Spriggs is 44

As you can see, we pass in whichever parts of the data we want to change, and the rest remains the same.

I started off in LinqPad, as this allows for quick development and testing of code like this. I created a couple of classes to use for testing…

class Person {
  public int ID { get; set; }
  public string FirstName { get; set; }
  public string Surname { get; set; }
  public Address Address { get; set; }
  public int Age { get; set; }
  public DateTime DateOfBirth { get; set; }
  public DateTime? DateOfDeath { get; set; }
  public string[] Nicknames { get; set; }
  public List<string> Nocknames { get; set; }
}

class Address {
  public int ID { get; set; }
  public string Address1 { get; set; }
  public string Address2 { get; set; }
  public string City { get; set; }
  public string Postcode { get; set; }
}

The idea was to write a method that would output the C# for the With() method. In order to help me understand what I needed to do, I first wrote a With() method manually, so I could see what my generating code should produce. For the Person class, this would look like this…

public Person With(int? id = null, string firstName = null,
                   string surname = null, Address address = null,
                   int? age = null, DateTime? dateOfBirth = null,
                   DateTime? dateOfDeath = null, string[] nicknames = null,
                   List<string> nocknames = null) =>
  new Person {
  ID = id ?? ID,
  FirstName = firstName ?? FirstName,
  Surname = surname ?? Surname,
  Address = address ?? Address,
  Age = age ?? Age,
  DateOfBirth = dateOfBirth ?? DateOfBirth,
  DateOfDeath = dateOfDeath ?? DateOfDeath,
  Nicknames = nicknames ?? Nicknames,
  Nocknames = nocknames ?? Nocknames
}

Any parameters that are not specified will be null, and so the original value used. Note that this isn’t perfect, as if you have nullable types in your class (like the DateOfDeath property, which is of type DateTime?, as the person may still be alive), you won’t be able to do the following…

Person jimsTwin = jim.With(dateOfDeath: null);

I never got around this.

However, I carried on and came up with the following…

string GenerateWith(Type t) {
  string with = $"public {t.Name} With(";
  string pars = string.Join(", ", t.GetProperties()
    .Select(p => $"{GetNullableTypeAlias(p.PropertyType)} {CamelCase(p.Name)} = null"));
  with += pars + ") =>" + Environment.NewLine;
  with += $"  new {t.Name} {{" + Environment.NewLine;
  string setters = string.Join("," + Environment.NewLine, t.GetProperties()
    .Select(p => $"  {p.Name} = {CamelCase(p.Name)} ?? {p.Name}"));
  with += setters + Environment.NewLine;
  with += "}";
  return with;
}

string CamelCase(string s) =>
  s == "ID" ? "id" : Char.ToLower(s[0]) + s.Substring(1);

string GetNullableTypeAlias(Type type) {
  string t = GetTypeAlias(type);
  if (t == "string") {
    return t;
  }
  if (t.EndsWith("[]")) {
    return t;
  }
  if (t.Contains("<")) {
    return t;
  }
  if (type.IsClass) {
    return t;
  }
  return t + "?";
}

string GetTypeAlias(Type type) {
  Dictionary<Type, string> _typeAlias = new Dictionary<Type, string>{
    { typeof(bool), "bool" },
    { typeof(byte), "byte" },
    { typeof(char), "char" },
    { typeof(decimal), "decimal" },
    { typeof(double), "double" },
    { typeof(float), "float" },
    { typeof(int), "int" },
    { typeof(long), "long" },
    { typeof(object), "object" },
    { typeof(sbyte), "sbyte" },
    { typeof(short), "short" },
    { typeof(string), "string" },
    { typeof(uint), "uint" },
    { typeof(ulong), "ulong" },
    // Yes, this is an odd one.  Technically it's a type though.
    { typeof(void), "void" }
  };
  // Nullables
  Type nullbase = Nullable.GetUnderlyingType(type);
  if (nullbase != null) {
    return GetTypeAlias(nullbase);
  }
  // Arrays
  if (type.BaseType == typeof(System.Array)) {
    return GetTypeAlias(type.GetElementType()) + "[]";
  }
  // Generics
  if (type.IsGenericType) {
    string name = type.Name.Split('`').FirstOrDefault();
    IEnumerable<string> parms = type.GetGenericArguments()
      .Select(a => type.IsConstructedGenericType ? GetTypeAlias(a) : a.Name);
    return $"{name}<{string.Join(",", parms)}>";
  }
  return _typeAlias.TryGetValue(type, out string alias) ? alias : type.Name;
}

If you run the following…

Console.WriteLine(GenerateWith(typeof(Person)));

…you’ll find it outputs the method I showed above.

So, subject to my caveat, I now had a method for generating the With() method. The problem is that it’s very manual. I could do with a way of having these generated automatically.

Any .NET programmer worth their salt will of course be screaming “T4 templates!” at me by now. Yup, that’s exactly where I went next.

I had always found the world of T4 to be very enticing, but very hard to comprehend. MSDN is (as ever) terse, dry and hard to follow when you’re trying to learn, and the online samples I’d seen didn’t really explain what it was about. That all changed when I read Metaprogramming in .NET by Kevin Hazzard and Jason Bock (hmm, this is starting to sound like an advert for Manning!). One chapter in this (excellent) book was dedicated to T4 templates. After a brief history of why we have them and how we got them, they give a really clear explanation of how to use them.

The one thing that the book didn’t explain clearly was how to have one template output multiple files. Thankfully I found this rather excellent blog post by Damien Guard, in which he explained how he had written a T4 helper that allowed us to do just that. Basically, all you need to is include the MultipleOutputHelper.ttinclude file he wrote, and away you go.

The resulting .tt file looked like this…

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ include file="MultipleOutputHelper.ttinclude" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".log" #>

<# var manager = Manager.Create(Host, GenerationEnvironment); #>
<# manager.StartHeader(false); #>
<# manager.EndBlock(); #>

<#
  IServiceProvider hostServiceProvider = (IServiceProvider)Host;
  EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
  Array activeSolutionProjects = (Array)dte.ActiveSolutionProjects;
  EnvDTE.Project dteProject = (EnvDTE.Project)activeSolutionProjects.GetValue(0);
  string defaultNamespace = dteProject.Properties.Item("DefaultNamespace").Value.ToString();
  string templateDir = Path.GetDirectoryName(Host.TemplateFile);
  string fullPath = dteProject.Properties.Item("FullPath").Value.ToString();
  fullPath = fullPath.EndsWith("\\") ? fullPath.Substring(0, fullPath.Length-1) : fullPath;
  string exePath = fullPath + "\\bin\\Debug\\" + defaultNamespace + ".exe";
  string subNamespace = templateDir.Replace(fullPath, string.Empty).Replace("\\", ".");
  string fileNamespace = string.Concat(defaultNamespace, subNamespace);

  string path = this.Host.ResolveAssemblyReference("$(TargetPath)");
  Assembly asm = Assembly.Load(File.ReadAllBytes(exePath));

  foreach(string file in Directory.GetFiles(templateDir, "*.cs").Where(f => !f.EndsWith("Ext.cs")).Select(f => f.Replace(".cs", ""))) {
    manager.StartNewFile($"{file}Ext.cs"); 
    string className = Path.GetFileNameWithoutExtension(file);

    Type t = asm.GetType(fileNamespace + "." + className);
    string pars = string.Join(", ", t.GetProperties().Select(p => $"{GetNullableTypeAlias(p.PropertyType)} {CamelCase(p.Name)} = null"));
    string setters = string.Join("," + Environment.NewLine + "      ", t.GetProperties().Select(p => $"  {p.Name} = {CamelCase(p.Name)} ?? {p.Name}"));

#>
// This code was generated from a template, do not modify
// Source file: <#=file#>.cs

using System;
using System.Collections.Generic;

namespace <#=fileNamespace#> {
  public partial class <#=className#> {
    public <#=className#> With(<#=pars#>) =>
      new <#=className#> {
        <#=setters#>
      };
  }
}
<#
manager.EndBlock();
}
#>

<#
manager.Process(true); 
asm = null;
#>

<#+
string CamelCase(string s) =>
  s == "ID" ? "id" : Char.ToLower(s[0]) + s.Substring(1);

string GetNullableTypeAlias(Type type) {
  string t = GetTypeAlias(type);
  if (t == "string") {
    return t;
  }
  if(t.EndsWith("[]")){
    return t;
  }
  if (t.Contains("<")) {
    return t;
  }  
  if(type.IsClass){
    return t;
  }  
  return t + "?";
}

string GetTypeAlias(Type type) {
  Dictionary<Type, string> _typeAlias = new Dictionary<Type, string>{
    { typeof(bool), "bool" },
    { typeof(byte), "byte" },
    { typeof(char), "char" },
    { typeof(decimal), "decimal" },
    { typeof(double), "double" },
    { typeof(float), "float" },
    { typeof(int), "int" },
    { typeof(long), "long" },
    { typeof(object), "object" },
    { typeof(sbyte), "sbyte" },
    { typeof(short), "short" },
    { typeof(string), "string" },
    { typeof(uint), "uint" },
    { typeof(ulong), "ulong" },
    // Yes, this is an odd one.  Technically it's a type though.
    { typeof(void), "void" }
  };
  // Nullables
  Type nullbase = Nullable.GetUnderlyingType(type);
  if (nullbase != null) {
    return GetTypeAlias(nullbase);
  }
  // Arrays
  if (type.BaseType == typeof(System.Array)) {
    return GetTypeAlias(type.GetElementType()) + "[]";
  }
  // Generics
  if (type.IsGenericType) {
    string name = type.Name.Split('`').FirstOrDefault();
    IEnumerable<string> parms = type.GetGenericArguments().Select(a => type.IsConstructedGenericType ? GetTypeAlias(a) : a.Name);
    return $"{name}<{string.Join(",", parms)}>";
  }
  return _typeAlias.TryGetValue(type, out string alias) ? alias : type.Name;
}
#>

If you compare this to the LinqPad code I showed earlier, you’ll see that it’s very similar. I had to add some extra code as I was in a T4 template, but you should be able to work out what that does by reading the metaprogramming book and Damien Guard’s blog post.

The template assumes that the classes you want to extend are in the same folder as the .tt file. This is a reasonable assumption if you were using an Entity Framework model to generate the classes, and had the .edmx file in a folder. The template creates a partial class for each class in the folder, where the new file is named (say) PersonExt.cs for the Person.cs file. For example, in the Visual Solution project I created to develop the .tt file, I had a Models folder, to which I added the Person and Address classes shown above…

As you can see, the T4 template generated PersonExt.cs and AddressExt.cs in the same folder. Visual Studio shows them nested under the .tt file, but if you look in File Explorer, you’ll see that they are just plain files…

Now, whenever you modify any of the classes in the Models folder, you can just right-click the .tt file and choose “Run custom tool” to have it regenerate the AbcExt partial classes.

Postscript: I wrote the code above last March, but never got around to blogging about it. I just discovered that one of the features slated to make it into C#9 is, you guessed it, the “with” keyword. Reading that blog, it’s very obvious that C# is becoming more and more functional with every release. With the exception of top-level programs (neat, but not earth-shattering), every feature mentioned in that blog post is a major step towards making C# a first-class functional language.

The future is bright, the future is functional!

Be First to Comment

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.