Last year I wrote a series about using strongly typed IDs to avoid a whole class of bugs in C# applications. In this post I describe some recent updates to a NuGet package that drastically reduces the amount of boilerplate you have to write by auto-generating it for you at compile-time.
Background
If you don't know what strongly typed IDs are about, I suggest reading the previous posts in this series. In summary, strongly-typed IDs help avoid bugs introduced by using primitive types for entity identifiers. For example, imagine you have a method signature like the following:
public Order GetOrderForUser(Guid orderId, Guid userId);
Can you spot the bug in the method call?
public Order GetOrder(Guid orderId, Guid userId)
{
return _service.GetOrderForUser(userId, orderId);
}
The call above accidentally inverts the order of orderId
and userId
when calling the method. Unfortunately, the type system doesn't help us here because both IDs are using the same type, Guid
.
Strongly Typed IDs allow you to avoid these types of bugs entirely, by using different types for the entity IDs, and using the type system to best effect. This is something that's easy to achieve in some languages (e.g. F#), but is a bit of a mess in C# (at least until we get record types in C# 9!):
public readonly struct OrderId : IComparable<OrderId>, IEquatable<OrderId>
{
public Guid Value { get; }
public OrderId(Guid value)
{
Value = value;
}
public static OrderId New() => new OrderId(Guid.NewGuid());
public bool Equals(OrderId other) => this.Value.Equals(other.Value);
public int CompareTo(OrderId other) => Value.CompareTo(other.Value);
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is OrderId other && Equals(other);
}
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(OrderId a, OrderId b) => a.CompareTo(b) == 0;
public static bool operator !=(OrderId a, OrderId b) => !(a == b);
}
The StronglyTypedId NuGet package massively simplifies the amount of code you need to write to the following:
[StronglyTypedId]
public partial struct OrderId { }
On top of that, the StronglyTypedId package uses Roslyn to auto generate the additional code whenever you save a file. No need for snippets, full IntelliSense, but all the benefits of strongly-typed IDs!
So that's the background, now lets look at some of the updates
Recent updates
These updates are primarily courtesy of Bartłomiej Oryszak who did great work! There are primarily three updates:
- Update to the latest version of CodeGeneration.Roslyn to support for .NET Core 3.x
- Support creating JSON converters for System.Text.Json
- Support for using long as a backing type for the strongly typed ID
Support for .NET Core 3.x
StronglyTypedId has now been updated to the latest version of CodeGeneration.Roslyn to support for .NET Core 3.x. This brings updates to the Roslyn build tooling, which makes the library much easier to consume. You can add a single <PackageReference>
in your project.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StronglyTypedId" Version="0.2.1" PrivateAssets="all"/>
</ItemGroup>
</Project>
Setting
PrivateAssets=all
prevents the CodeGeneration.Roslyn.Attributes and StronglyTypedId.Attributes from being published to the output. There's no harm in them being there, but they're only used at compile time!
With the package added, you can now add the [StronglyTypedId]
to your IDs:
[StronglyTypedId(generateJsonConverter: false)]
public partial struct OrderId {}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Value = " + new OrderId().Value);
}
}
This will generate a Guid
-backed ID, with a TypeConverter
, without any JSON converters. If you do want explicit JSON converters, you have another option—System.Text.Json converters.
Support for System.Text.Json converters
StronglyTypedId has always supported the Newtonsoft.Json JsonConverter
but now you have another option, System.Text.Json. You can generate this converter by passing an appropriate StronglyTypedIdJsonConverter
value:
[StronglyTypedId(jsonConverter: StronglyTypedIdJsonConverter.SystemTextJson)]
public partial struct OrderId {}
This generates a converter similar to the following:
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonConverter(typeof(OrderIdSystemTextJsonConverter))]
readonly partial struct OrderId : IComparable<OrderId>, IEquatable<OrderId>
{
// other implementation
public class OrderIdSystemTextJsonConverter : JsonConverter<OrderId>
{
public override OrderId Read(ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options)
{
return new OrderId(System.Guid.Parse(reader.GetString()));
}
public override void Write(Utf8JsonWriter writer, OrderId value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Value);
}
}
}
If you want to generate both a System.Text.Json converter and a Newtonsoft.Json converter, you can use flags:
[StronglyTypedId(jsonConverter: StronglyTypedIdJsonConverter.SystemTextJson | StronglyTypedIdJsonConverter.NewtonsoftJson)
public partial struct OrderId {}
Remember, if you generate a Newtonsoft.Json converter, you'll need to add a reference to the project file.
Support for long as a backing type
The final update is adding support for using long
as the backing field for your strongly typed IDs. To use long
, use the StronglyTypedIdBackingType.Long
option:
[StronglyTypedId(backingType: StronglyTypedIdBackingType.Long)]
public partial struct OrderId {}
The currently supported backing fields are:
Guid
int
long
string
Future work
C# 9 is bringing some interesting features, most notably source generators and record types. Both of these features have the potential to impact the StronglyTypedId package in different ways.
Source generators are designed for exactly the sort of functionality StronglyTypedId provides - build time enhancement of existing types. From a usage point of view, as far as I can tell, converting to using source generators would provide essentially the exact same experience as you can get now with CodeGeneration.Roslyn. For that reason, it doesn't really seem worth the effort looking into at this point, unless I've missed something!
Record types on the other hand are much more interesting. Records provide exactly the experience we're looking for here! With the exception of the built-in TypeConverter
and JsonConverter
s, records seem like they would give an overall better experience out of the box. So when C#9 drops, I think this library can probably be safely retired 🙂
Summary
In this post I described some recent enhancements to the StronglyTypedId NuGet package, which lets you generate strongly-typed IDs at compile time. The updates simplify using the StronglyTypedId package in your app by supporting .NET Core 3.x, added support for System.Text.Json
as a JsonConverter
, and using long
as a backing field. If you have any issues using the package, let me know in the issues on GitHub, or in the comments below.
Top comments (0)