STRINGLY-typed obsession
Tags: csharp, roslyn
Published: 2023-01-20 21:58:35

Aren't we all tired of **STRINGLY**-typed values? Have you heard about Primitive Obsession anti-pattern?

It's time to stop the suffering (at least in a strongly-typed .NET world).

**Make compiler your friend, introduce semantics to your code. Eliminate stringly-typed interfaces.**

## Example #1

Imagine, we need to call an API. There is an endpoint that returns info about a TV show. We start with exploratory testing and make an API call.

> GET https://api.tvmaze.com/shows/1
>
> *[TVmaze API](https://www.tvmaze.com/api/) is licensed by [CC BY-SA](https://creativecommons.org/licenses/by-sa/4.0/).*

```json
{
    "id": 1,
    "url": "https://www.tvmaze.com/shows/1/under-the-dome",
    "name": "Under the Dome",
    "type": "Scripted",
    "language": "English",
    "genres": [
        "Drama",
        "Science-Fiction",
        "Thriller"
    ],
    "status": "Ended",
    "runtime": 60,
    "averageRuntime": 60,
    "premiered": "2013-06-24",
    "ended": "2015-09-10",
    "officialSite": "http://www.cbs.com/shows/under-the-dome/",
    "schedule": {
        "time": "22:00",
        "days": [
            "Thursday"
        ]
    },
    "rating": {
        "average": 6.5
    },
    "weight": 98,
    "network": {
        "id": 2,
        "name": "CBS",
        "country": {
            "name": "United States",
            "code": "US",
            "timezone": "America/New_York"
        },
        "officialSite": "https://www.cbs.com/"
    },
    "webChannel": null,
    "dvdCountry": null,
    "externals": {
        "tvrage": 25988,
        "thetvdb": 264492,
        "imdb": "tt1553656"
    },
    "image": {
        "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/81/202627.jpg",
        "original": "https://static.tvmaze.com/uploads/images/original_untouched/81/202627.jpg"
    },
    "summary": "<p><b>Under the Dome</b> is the story of a small town that is suddenly and inexplicably sealed off from the rest of the world by an enormous transparent dome. The town's inhabitants must deal with surviving the post-apocalyptic conditions while searching for answers about the dome, where it came from and if and when it will go away.</p>",
    "updated": 1631010933,
    "_links": {
        "self": {
            "href": "https://api.tvmaze.com/shows/1"
        },
        "previousepisode": {
            "href": "https://api.tvmaze.com/episodes/185054"
        }
    }
}
```

What do we usually do next?

Right, we go to something like [https://json2csharp.com](https://json2csharp.com/) and copy-pasting the JSON there. As a result, we've got a nice DTO that we can use in our C# code.

If we do minor fixes to the root class and property names casing, it'll look something like this:

```C#
    // Show show = JsonConvert.DeserializeObject<Show>(json);

    public class Show
    {
        public int Id { get; set; }
        public string Url { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public string Language { get; set; }
        public List<string> Genres { get; set; }
        public string Status { get; set; }
        public int Runtime { get; set; }
        public int AverageRuntime { get; set; }
        public string Premiered { get; set; }
        public string Ended { get; set; }
        public string OfficialSite { get; set; }
        public Schedule Schedule { get; set; }
        public Rating Rating { get; set; }
        public int Weight { get; set; }
        public Network Network { get; set; }
        public object WebChannel { get; set; }
        public object DvdCountry { get; set; }
        public Externals Externals { get; set; }
        public Image Image { get; set; }
        public string Summary { get; set; }
        public int Updated { get; set; }
        public Links _links { get; set; }
    }

    public class Country
    {
        public string Name { get; set; }
        public string Code { get; set; }
        public string Timezone { get; set; }
    }

    public class Externals
    {
        public int TvRage { get; set; }
        public int TheTvDb { get; set; }
        public string Imdb { get; set; }
    }

    public class Image
    {
        public string Medium { get; set; }
        public string Original { get; set; }
    }

    public class Links
    {
        public Self Self { get; set; }
        public PreviousEpisode PreviousEpisode { get; set; }
    }

    public class Network
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Country Country { get; set; }
        public string OfficialSite { get; set; }
    }

    public class PreviousEpisode
    {
        public string Href { get; set; }
    }

    public class Rating
    {
        public double Average { get; set; }
    }

    public class Schedule
    {
        public string Time { get; set; }
        public List<string> Days { get; set; }
    }

    public class Self
    {
        public string Href { get; set; }
    }
```

Do you see the problem now?

All values are primitive:
- Can you tell the difference between `Scripted` and `English`?
- What about `Ended` and `Drama`?
- Some of them can be not `string` but `int`. Does it help? What's the difference between ID with value `1` and Runtime with value `60`?

It's very easy to mix up two values of the same type whether they are `string` or `int`.

Humans make errors. And compilers are here to help. But it can help with a bunch of `string` values.


![Shift left](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkwmzl6ku9yrhjt5983k.png)

If a developer mixes up `Drama` and `English`, the problem can be propagated to Production and might be discovered by an end-user 😢

It's bad when users report problems. It's very late and quite expensive to fix. The earlier we discover issues, the better. The earliest possible time is when a developer typing the code and compiler checks it on the fly in the IDE.

## Example #2

Let's say we are working on an integration between two systems. We fetch an entity from API #1 and put it to API #2.

> GET https://service1/api/orders/2a14d479-0d74-4742-91b6-fbae62ddc017
> 
> ```JSON
> {
>     "orderId": "2a14d479-0d74-4742-91b6-fbae62ddc017",
>     "orderTrackingId": "2bec51f6-f11f-476e-b336-d94426d25a55",
>     "productId": "31ef8a4c-face-49dd-b521-fc930a026848",
>     "personId": "f1d9f71d-dd0c-444e-9537-2a301fb94e1f"
> }
> ```
>
> POST https://target-service/api/orders/2bec51f6-f11f-476e-b336-d94426d25a55
> 
> ```JSON
> {
>     "order": {
>         "orderId": "2a14d479-0d74-4742-91b6-fbae62ddc017",
>         "orderTrackingId": "2bec51f6-f11f-476e-b336-d94426d25a55",
>         "personId": "31ef8a4c-face-49dd-b521-fc930a026848",
>         "productId": "f1d9f71d-dd0c-444e-9537-2a301fb94e1f"
>     }
> }
> ```

**Have you spotted the problem?** 👀

**Have you found another one?** 😲

You might think it's a silly example. But it's not.

It happens.

Humans make mistakes and IDE can unintentionally help with that.

### How could it happen?

You might ask:

> "How is it possible?! I am an experienced developer, I don't make silly mistakes".

We all do.

#### First bug

I was typing `ProductId`, I was tired, IDE suggested an auto-completion, and I agreed:

![Auto-complete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sa5hjtlh69uxgzie4o9z.png)

💥 Boom. We've got a `PersonId` instead of a `ProductId`.

- They are both GUIDs,
- look similar,
- same length,
- start with `P`,
- both IDs.

#### Second bug

If you haven't noticed, there is another less discoverable bug.

We send a POST request with the wrong ID. I did the same thing. I was trying to type `.OrderId` but somehow ended up with `.OrderTrackingId`.

It's even harder to notice.

🙋‍♀️ Hey, compiler! Where are you, my friend? Why don't you help me here? Why are you so dumb? (*A mean person might say*)

## What if the compiler were smarter?

**Should we blame compiler?** I don't think so.

**Can we do better?** Sure, we can 💪

We, as developers, can help the compiler to distinguish `PersonId` and `ProductId`. Despite both being GUIDs, they have completely different meanings.

And complier is very good with semantics. It just requires some small help.

How does compiler distinguish `1.5` and `"1.5"`? There is a type system in the language:

- `1.5` is a `float`
- `"1.5"` is a `string`

What if C# would allow us to make different aliases for the same types? And what if it will treat them as different types then?

```C#
var personId = new PersonId("f1d9f71d-dd0c-444e-9537-2a301fb94e1f");
var productId = new ProductId("31ef8a4c-face-49dd-b521-fc930a026848");

personId = productId;
//       ^ Compile-time error: Cannot convert source type 'ProductId' to target type 'PersonId'
```

That's what I expect from a nice compiler. But without too much hassle from my side.

Can we do it? ❓❓❓

## Wonderfully-typed world

That is a healthy strongly-typed class. Easy job for compiler to help me to spot mistakes immediately, in a blink of an eye 🤩

```C#
public class Show
{
    public ShowId Id { get; set; }
    public ShowUri Url { get; set; }
    public ShowName Name { get; set; }
    public ShowType Type { get; set; }
    public ShowLanguage Language { get; set; }
    public List<Genres> Genres { get; set; }
    public ShowStatus Status { get; set; }
    public Runtime Runtime { get; set; }
    public Runtime AverageRuntime { get; set; }
    public PremieredDate Premiered { get; set; }
    public EndedDate Ended { get; set; }
    public OfficialSiteUri OfficialSite { get; set; }
    public Schedule Schedule { get; set; }
    public Rating Rating { get; set; }
    public Weight Weight { get; set; }
    public Network Network { get; set; }
    public WebChannel WebChannel { get; set; }
    public DvdCountry DvdCountry { get; set; }
    public Externals Externals { get; set; }
    public Image Image { get; set; }
    public string Summary { get; set; }
    public UpdatedTimestamp Updated { get; set; }
    public Links _links { get; set; }
}
```

What does it take on my side to enable that?

I just need to do a little boring job. I need to create a class per type and do a little magic.

```C#
[StrongType(typeof(int))]
public partial class ShowId
{
}

[StrongType(typeof(int))]
public partial class PersonId
{
}

[StrongType(typeof(Uri))]
public partial class ShowUri
{
}

[StrongType] // `string` by default
public partial class ShowName
{
}

// ... And so on

```

And you will never mix up Show ID and Person ID again.

> 😎 Just use the [Xtz.StronglyTyped](https://www.nuget.org/packages/Xtz.StronglyTyped) NuGet package. E-a-s-y:
>
> `<PackageReference Include="Xtz.StronglyTyped" Version="0.23.0" />`

**This NuGet package is a practical solution for the primitive obsession problem.**

## What a magical thing! ✨

Somebody was obsessed with the primitive obsession, so they took some time to figure out how to do the magic.

Behind `[StrongType]` attribute, there is some trickery done.

Trick is possible because of Roslyn – a modern C# compiler. *It was introduced back in 2014 in Visual Studio 2015 RTM*.

There is a very useful Roslyn feature – Source Generators. You can create a Source Generator and plug it in to your C# project (as a Roslyn analyzer).

The Source Generator kicks in before compilation process, analyzes your code, finds all types with `[StrongType]` attribute, generates some C# code for them. And then the code is compiled.

In such a way, we can do a lot of compiler-time magic to facilitate strongly-typed values and eliminate primitive obsession.

The compiler is our best friend again 😍

## More examples

### ✅ Strongly-typed GUID-based IDs

Just use `EmployeeGuidId` to generate new GUIDs and keep the types strong. Or parse it from `string`, or create it from `Guid`.

```C#
[StrongType(typeof(Guid))]
public partial class EmployeeGuidId : GuidId
{
}

// ...

// Generating a new GUID
var employee1Id = new EmployeeGuidId(); // 58a221d3-59e3-4038-ae26-5b728d331b8b

// Generating a new GUID
var employee2Id = new EmployeeGuidId(); // 92ad2a40-994f-4e92-b427-db8f707699bd

// Parsing a GUID from a string value
var employee3Id = new EmployeeGuidId("cd5944c7-a649-41c6-a8e9-b4dd218f6b1f");
// Passing GUID to constructor
employee3Id = new EmployeeGuidId(Guid.Parse("cd5944c7-a649-41c6-a8e9-b4dd218f6b1f"));

// Creating the same IDs
var id = "1169883c-5f0b-4b65-81cb-65c86ce5794d";
var id1 = new EmployeeGuidId(guid);
var id2 = new EmployeeGuidId(guid);
// Comparing them
Console.WriteLine(id1 == id2); // True
```

### ✅ Strongly-typed int-based IDs

Just convert an `int` to an Employee ID when needed.

```C#
[StrongType(typeof(int))]
public partial class EmployeeIntId : IntId
{
}

// ...

// Explicit conversion
var employee1Id = (EmployeeIntId)4682;

// Creation using constructor
var employee2Id = new EmployeeIntId(3958);

// Back to primitives: Convert to `int` (just in case if needed)
var intId1 = Convert.ToInt32(employee2Id); // 3958
// Back to primitives: Explicit conversion
int intId2 = (int)employee2Id; // 3958

```

### ✅ Nice debugger display

When you are in a debug mode, you see the values straight away, without digging in.

![Debugger display 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5weyaq0235ydqbyjwwv.png)

![Debugger display 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tttlm7apis9n1es3yaja.png)

### ✅ Strongly-typed JSON

Parsing JSON to strong types. Take JSON:

```JSON
{
    "filter": {
        "country": "The Netherlands"
    }
}
```

And simply deserialize it:

```C#
[StrongType]
public partial class Country
{
}

public class Filter
{
    public Country Country { get; init; }
}

// ...

var result = JsonSerializer.Deserialize<Filter>(json);
```

### ✅ Strongly-typed `appsettings.json`

Create `appsettings.json`:

```JSON
{
    "ShowsApi": {
        "filter": {
            "country": "The Netherlands"
        }
    }
}
```

Configure DI and inject `IOptions<>`:

```C#
public class FilterSettings
{
    public Country Country { get; set; }
}

[StrongType]
public partial class Country
{
}

// ... Setting up Configuration and DI

public class FilterService
{
    // Inject using proper strongly-typed DI
    public FilterService(IOptions<FilterSettings> filterSettings)
    {
        // ...
    }
}

```

### Built-in types

Use handy predefined types from `Xtz.StronglyTyped.BuiltinTypes`.

> 😎 Just use the [Xtz.StronglyTyped.BuiltinTypes](https://www.nuget.org/packages/Xtz.StronglyTyped.BuiltinTypes) NuGet package. E-a-s-y:
>
> `<PackageReference Include="Xtz.StronglyTyped.BuiltinTypes" Version="0.19.0" />`

Example of built-in strong types:

```C#
// Built-in types

var country = new Country("Canada");
var jobKey = new JobKey("DX-857");
var currencyCode = new CurrencyCode("EUR");
```

Types with an extra magic:

```C#
// Uses `System.Uri` as an inner type
var uri = new AbsoluteUri("https://example.com");

// Uses `System.Net.IPAddress` as an inner type
var ipAddress = new IpV4Address("127.0.0.1");

// Uses `System.Net.Mail.MailAddress` as an inner type
var email = new Email("john@example.com");

// Just a plain `string` as inner type but with a note of magic
var uppercased = new UpperCased("all-caps strong type"); // "ALL-CAPS STRONG TYPE"
```

Types with built-in runtime validation (from inner types):

```C#
// Uses `System.Uri` as an inner type for validation
var uri1 = new AbsoluteUri("/api");
//         ^ System.UriFormatException: 'Invalid URI: The format of the URI could not be determined.'

// Uses `System.Uri` as an inner type for validation
var uri2 = new RelativeUri("https://example.com");
//         ^ System.UriFormatException: 'A relative URI cannot be created because the 'uriString' parameter represents an absolute URI.'

// Uses `System.Net.Mail.MailAddress` as an inner type for validation
var email = new Email("incorrect-value");
//          ^ System.FormatException: 'The specified string is not in the form required for an e-mail address.'

// Uses `System.Net.IPAddress` as an inner type for validation
var ipAddress = new IpV4Address("999.0.0.1");
//              ^ System.FormatException: 'An invalid IP address was specified.'

```

### ✅ Auto-generated values for Unit Tests based on `AutoData` and `Bogus`

Use `[StrongAutoData]` magic.

> 😎 Just use the [Xtz.StronglyTyped.BuiltinTypes.AutoFixture](https://www.nuget.org/packages/Xtz.StronglyTyped.BuiltinTypes.AutoFixture) NuGet package. E-a-s-y:
>
> `<PackageReference Include="Xtz.StronglyTyped.BuiltinTypes.AutoFixture" Version="0.19.0" />`

Magical ingredients:

- `AutoFixture` NuGet and `[AutoData]` attribute – to automatically inject data to the test cases
- `Bogus` NuGet – to generate realistic values
- `Xtz.StronglyTyped.BuiltinTypes.AutoFixture` NuGet – to glue `AutoFixture` and `Bogus` together along with strongly-typing the values

#### Numbers

```C#
using Xtz.StronglyTyped.BuiltinTypes.AutoFixture;
using Xtz.StronglyTyped.BuiltinTypes.Numbers;

[Test]
[StrongAutoData]
public void ShouldGenerateStronglyTypedValues(

    // Randomly-generated values injected into tests

    OddInt32 oddInt32,
    NonPositiveInt32 nonPositiveInt32)
{
    Console.WriteLine(oddInt32); // 94575 or any other random odd `Int32`
    Console.WriteLine(nonPositiveInt32); // -67950 or any other random non-positive `Int32`
}
```

> ❗ Could you imagine compiler-checked odd numbers?! Here we go!

#### Automotive types

```C#
[Test]
[StrongAutoData]
public void ShouldGenerateStronglyTypedValues(

    // Randomly-generated with meaningful values

    FuelType fuelType, // Gasoline
    VehicleManufacturer vehicleManufacturer, // BMW
    VehicleModel vehicleModel, // Model 3
    VehicleType vehicleType, // SUV
    Vin vin) // B6RUK1QFK8AR56121
{
    // ...
}
```

#### Names

```C#
[Test]
[StrongAutoData]
public void ShouldGenerateStronglyTypedValues(
    DisplayName displayName, // Syble Torphy
    FirstName firstName, // Odie
    LastName lastName, // O'Kon
    NamePrefix namePrefix, // Dr.
    NameSuffix nameSuffix) // Jr.
{
    // ...
}
```

## Conclusion

- Primitive obsession can bring a lot of troubles.
- It prevents benefits of the strongly-typed language.
- Code is less readable nor maintainable.

Using `Xtz.StronglyTyped` libraries we can:

- Employ compiler to do the type checks. Never pass the wrong ID or name anymore.
- Use primitive types as inner types (i.e., `string`, `int`).
- Use .NET types as inner types (i.e., `Guid`, `Uri`, `MailAddress`, `IPAddress`, etc.).
- Use `IntId` and `GuidId` as base classes for our IDs.
- Save time with built-in types (i.e., `Email`, `FirstName`, `Vin`, `Country`, `JobKey`, etc.)
- Use strong types with extra runtime magic (i.e., `NonPositiveInt32`, `UpperCased`, etc.)
- Create own types with custom runtime validation.
- Parse strong types from JSON.
- Use strong types in `appsettings.json` with `IOptions<T>`.
- Store strong types via Entity Framework.
- Inject randomly-generated strongly-typed values to unit tests.

> 😎 Just use the [Xtz.StronglyTyped](https://www.nuget.org/packages/Xtz.StronglyTyped) NuGet package. E-a-s-y:
>
> `<PackageReference Include="Xtz.StronglyTyped" Version="0.23.0" />`
>
> These NuGet packages provide extra help:
>
> - [Xtz.StronglyTyped.BuiltinTypes](https://www.nuget.org/packages/Xtz.StronglyTyped.BuiltinTypes)
> - [Xtz.StornglyTyped.BuiltinTypes.AutoFixture](https://www.nuget.org/packages/Xtz.StronglyTyped.BuiltinTypes.AutoFixture)
> - [Xtz.StornglyTyped.BuiltinTypes.Bogus](https://www.nuget.org/packages/Xtz.StronglyTyped.BuiltinTypes.Bogus)
> - [Xtz.StornglyTyped.EntityFramework](https://www.nuget.org/packages/Xtz.StronglyTyped.EntityFramework)
> - [Xtz.StornglyTyped.Swashbuckle](https://www.nuget.org/packages/Xtz.StronglyTyped.Swashbuckle)

## Check out

- 👩‍💻 The source code: [dev-experience/Xtz.StronglyTyped](https://github.com/dev-experience/Xtz.StronglyTyped)
- 🧐 Sample show-case projects [dev-experience/Xtz.StronglyTyped.SampleProjects](https://github.com/dev-experience/Xtz.StronglyTyped.SampleProjects)

Feedback is welcome. Feel free to reach out.

Enjoy the types,
  Vlad