In this article, we’ll look at the StronglyTypedId NuGet package and how it helps us enhance type safety and code readability in our .NET projects by generating strongly-typed IDs for our entities.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started.

Benefits of Using StronglyTypedId

Before we look at how to install and utilize the StronglyTypedId NuGet package, let’s consider why we need it in the first place.

We commonly use data types like string, integer, or GUID to represent unique identifiers in our application. While these solutions are often effective, they can result in primitive obsession and major problems in our program in the future.

Using primitive data types as unique identifiers is extremely vulnerable to misuse and lacks context within our code. Consider mistakenly entering a UserId where a CommentId is expected. This can result in runtime errors, which are difficult to identify during development. In addition, while GUIDs are unique, they have no inherent meaning in our code.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

In contrast, StronglyTypedId is a custom type that serves as a unique identifier for our entity. It ensures type safety by defining separate classes for each identifier type, preventing unintentional ID mixing, and reducing runtime issues. Aside from preventing unintended ID mixing, it indicates their role clearly within our code. This makes the code more self-documenting and understandable to everyone.

Installing StronglyTypedId

To begin using the package, we have to install it first:

dotnet add package StronglyTypedId --version 1.0.0-beta08

Alternatively, we can install the package using Visual Studio’s NuGet package manager. As of this writing, the most recent stable version is 1.0.0-beta08.

Using StronglyTypedId

In a moment, we will see how to use the StronglyTypedId package. But first, let’s look at an example of how using primitive objects as unique identifiers in our program might lead to problems:

public class User
{
    public Guid Id { get; set; }
    public string? Name { get; set; }
    public List<Comment>? Comments { get; set; }
}

public class Comment
{
    public Guid Id { get; set; }
    public string? Text { get; set; }
    public Guid UserId { get; set; }
    public User? User { get; set; }
}

Here, we specify identifiers for the basic User and Comment entities using the Guid primitive. This is a common practice.

Now let’s create a basic method to retrieve a single user’s comment:

public class CommentService
{
    private static List<Comment> _comments =
    [
        new ()
        {
            Id = Guid.Parse("5c6a8f59-02c1-4f5b-bc85-bfea9ed3a6e4"),
            Text = "First comment made by user.",
            UserId = Guid.Parse("3b3b1a5d-6cd1-42f2-9331-fc6f716e9a2c")
        }
    ];

    public Comment GetSingleUserComment(Guid commentId, Guid userId)
    {
        var comment = _comments
            .FirstOrDefault(comment => comment.Id == userId
                            && comment.UserId == userId);

        return comment ?? throw new NullReferenceException();
    }
}

Notice the highlighted line in our GetSingleUserComment() method and the way userId is passed instead of commentId. This is done intentionally to check whether the program throws an error during compilation.

Unfortunately, this code compiles successfully and fails to throw any errors when running it, which is not the behavior we want in our program.

Now let’s see how we can resolve this issue, using the StronglyTypedId package to modify our User and Comment classes:

[StronglyTypedId]
public partial struct UserId { }

public class User
{
    public UserId Id { get; set; }
    public string? Name { get; set; }
    public List<Comment>? Comments { get; set; }
}

[StronglyTypedId]
public partial struct CommentId { }

public class Comment
{
    public CommentId Id { get; set; }
    public string? Text { get; set; }
    public UserId UserId { get; set; }
    public User? User { get; set; }
}

Here, we’re creating a struct for the Id properties of the User and Comment entities, and decorate both with the  [StronglyTypedId] attribute, to ensure that our IDs are unique.

Using our strongly typed IDs, let’s now modify the GetSingleUserComment() method:

public class CommentService
{
    private static List<Comment> _comments =
    [
        new ()
        {
            Id = new CommentId(),
            Text = "First comment made by user.",
            UserId = new UserId()
        }
    ];

    public Comment GetSingleUserComment(CommentId commentId, UserId userId)
    {
        var comment = _comments
            .FirstOrDefault(comment => comment.Id == userId
                            && comment.UserId == userId);

        return comment ?? throw new NullReferenceException();
    }
}

By assigning our unique IDs as types to the commentId and userId parameters, the editor will now throw an error if we mix up the IDs, leading to a compilation error:

Compilation error when incorrect type is used

This happens because the package source generator generates a method that compares the values of the IDs rather than the reference. This helps provide type safety and increased code readability when compared to just using a primitive data type. 

Advanced Features of StronglyTypedId

Although the basic usage demonstrates the core functionality, the StronglyTypedId package offers additional features that enhance the flexibility and customization of our IDs. Let’s see some of these features.

Using Different Types as Backing Fields for IDs

One of the key features of the package is the ability to use different data types as backing fields for IDs. By default, the package uses the Guid type for the underlying identifier but also allows us to choose the most suitable data type for storing ID values. We can specify the backing type using the string, long, and int types as well.

Here’s how we can modify the previous User entity to use a string as a backing field:

[StronglyTypedId(Template.String)]
public partial struct UserId { }

By giving a value from the Template enum in the constructor, we specify that the type of our UserId should be a string instead of the default Guid type. This allows us complete autonomy over the way we represent our IDs.

The built-in backing fields supported include int, long, Guid, and the string type we just saw. We can also benefit from templates built by others, by installing the StronglyTypedId templates package:

dotnet add package StronglyTypedId.Templates --version 1.0.0-beta08

Installing this package will allow us to reference all the templates that come with it.

Changing the Default Backing Field Globally

If we decide that the backing field for each ID in our project that uses the [StronglyTypedId] attribute should be the same, we can configure our project to do so:

[assembly: StronglyTypedIdDefaults(Template.String)]

[StronglyTypedId]
public partial struct UserId { }

[StronglyTypedId]
public partial struct CommentId { }

Here, we use the StronglyTypedIdDefaults assembly attribute one time to change the default backing field to type string for each ID.

We can override the global default by providing a backing field type for any StronglyTypedId attribute, as we did previously.

Conclusion

This article provides a quick overview of the StronglyTypedId NuGet package. We began by discussing some of the difficulties of utilizing primitive data types for entity identifiers. Then we covered how to use the package and define backing field types for individual IDs as well as globally.

To learn more about this package’s entire capability, visit the StronglyTypedId GitHub repository.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!