blog post image
Andrew Lock avatar

Andrew Lock

~10 min read

Adding JSON converters to strongly typed IDs

Using strongly-typed entity IDs to avoid primitive obsession - Part 2

In my previous post, I described a common problem in which primitive arguments (e.g. System.Guid or string) are passed in the wrong order to a method, resulting in bugs. This problem is a symptom of primitive obsession; using primitive types to represent higher-level concepts.

This post directly follow on from my previous post, so I strongly recommend reading that one first.

In my previous post I described a strongly-typed ID that could be used to represent the ID of an object, for example an OrderId or a UserId. As a reminder, the implementation looked something like this:

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());

    // various overloads, overrides, and implementations
}

One of the common complaints when fighting primitive obsession like this, is that it makes things more complex at the "edges" of the system, when converting between Guid and OrderId for example. The best answer to this is to try to use the strongly-typed IDs everywhere.

With the implementation described so far, this is easier said than done, so in this post I'll describe some helper classes you can use with strongly-typed IDs to make working with ASP.NET Core APIs simpler.

tl;dr; You can skip to a complete example implementation including all the converters or a Visual Studio snippet if you wish.

Strongly-typed IDs make for ugly JSON APIs

Lets imagine that you have a standard eCommerce app, as in the previous post. You have an MVC API controller for your Orders, containing a single Post action for creating Orders.

[ApiController]
public class OrderController : ControllerBase
{
    [HttpPost]
    public IActionResult Post(Order order);
}

As we have a strongly-typed IDs Orders and Users, the Order object now looks something like the following (instead of using Guids for IDs):

public class Order
{
    public OrderId Id { get; set; }
    public UserId UserId { get; set; }
    public decimal Total { get; set; }
}

The problem is that our strongly-typed IDs mean that for MVC Model Binding to work as we expect, the posted JSON body would have to look something like this:

{
    "Id": {
        "Value": "da63f7a0-a4a6-4dbe-a9a4-4bb72dde30dd"
    },
    "UserId": {
        "Value": "4bb20f98-f6d4-43bc-9fdf-5b74ce4ef751"
    },
    "Total": 123.45
}

Reading over this again, I actually don't think model binding would work at all in this case, though I haven't tested it since.

Urgh, that's a bit of a mess. Luckily, we can simplify this using a custom JsonConverter.

Creating a custom JsonConverter

JsonConverter in Newtonsoft.Json can be used to customise how types are converted to and from JSON. in ASP.NET Core 2.x that also allows you to customize how types are serialised and deserialised during model binding.

Note that in ASP.NET Core 3.0 JSON serialization will be changing. See this GitHub issue for details.

The following example shows how to create a JsonConverter as a nested class of the strongly-typed ID. I've hidden the bulk of the OrderId class for brevity, but make sure to decorate the main strongly-typed ID class with the [JsonConverter] attribute:

[JsonConverter(typeof(OrderIdJsonConverter))]
public readonly struct OrderId : IComparable<OrderId>, IEquatable<OrderId>
{
    // Strongly-typed ID implementation elided 

    class OrderIdJsonConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(OrderId);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var id = (OrderId)value;
            serializer.Serialize(writer, id.Value);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var guid = serializer.Deserialize<Guid>(reader);
            return new OrderId(guid);
        }
    }
}

Implementing the custom JsonConverter is relatively simple, and relies on the fact Newtonsoft.Json already knows how to serialize and deserialize Guids:

  • Override CanConvert: This converter can only convert OrderId types.
  • Override WriteJson: To serialize an OrderId, extract the Guid Value, and serialize that.
  • Override ReadJson: Start by deserializing a Guid, and create an OrderId from that.

By using a custom JsonConverter, the serialized Order looks much cleaner and easier to work with:

{
    "Id": "da63f7a0-a4a6-4dbe-a9a4-4bb72dde30dd",
    "UserId": "4bb20f98-f6d4-43bc-9fdf-5b74ce4ef751",
    "Total": 123.45
}

In fact, it's exactly the same as the original Order object was before we converted to strongly-typed IDs.

So that's the JSON support working, lets move on to looking at another API method, a GET method.

Using strongly-typed IDs in route constraints

A common pattern with REST APIs is to include the ID of a resource in the URL. For example:

[ApiController]
public class OrderController : ControllerBase
{
    [HttpGet("{id}")]
    public ActionResult<Order> Get(OrderId id);
}

In this example, you'd expect to be able to retrieve an Order object from the API by sending a GET request to /Order/7b-46-0c4, where 7b-46-0c4 is the ID of the order (shortened for brevity). Unfortunately, if you try this, you'll get a slightly confusing 415 Unsupported Media Type response:

{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.13","title":"Unsupported Media Type","status":415,"traceId":"0HLLI5VFOFT3C:00000003"}

Unsupported Media Type response

The problem is that the MVC framework doesn't know how to convert the string route segment "7b-46-0c4" into your OrderId type. We have a JSON converter that can convert strings to the OrderId type, but we're not converting from a JSON body in this case.

Creating a custom type converter

There's a couple of different ways you could solve this problem:

Creating a custom model binder is a relatively involved affair, but it gives you complete control over the binding process. In our case, we just need a simple string to OrderId conversion, and the documentation suggests you should use a type converter in this case.

Type converters provide "a unified way of converting types of values to other types". In our case, all we need to support is converting from a string to OrderId.

[JsonConverter(typeof(OrderIdJsonConverter))]
[TypeConverter(typeof(OrderIdTypeConverter))]
public readonly struct OrderId : IComparable<OrderId>, IEquatable<OrderId>
{
    // Strongly-typed ID implementation elided 

    class OrderIdTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var stringValue = value as string;
            if (!string.IsNullOrEmpty(stringValue)
                && Guid.TryParse(stringValue, out var guid))
            {
                return new OrderId(guid);
            }

            return base.ConvertFrom(context, culture, value);

        }
    }
}

Derive from the base TypeConverter class, and override the CanConvertFrom method to indicate that you can handle strings. I've created the implementation as a nested class of OrderId for tidiness.

In the ConvertFrom method override, cast the provided value to a string, and try to parse it into a Guid. If all goes well, you can return a new OrderId, otherwise, just delegate to the base implementation.

Finally, decorate your strongly-typed ID with the [TypeConverter] attribute, and reference your implementation.

That's all you need to fix your issues - no extra types to register with the MVC framework and no messing with custom model binding or providers. I was actually surprised how simple this approach was, having never used TypeConverters before!

Example of the request working

Martin Liversage noted that JSON.NET will use a TypeConverter where one exists, so you generally don't need the custom JsonConverter at all!

Other type converters for interfacing with the world.

With the two converters described above, you should be able to work seamlessly with your ASP.NET Core APIs, so there's no excuse for passing on using strongly-typed IDs there!

At the other end of the application, at the database, you may want to create similar converters. Given the number of possible ORMs and micro-ORMs, I won't go into the details here, but most will provide this functionality. For example, you can create a custom TypeHandler<T> for Dapper which would look something like the following:

class OrderIdTypeHandler : SqlMapper.TypeHandler<OrderId>
{
    public override void SetValue(IDbDataParameter parameter, OrderId value)
    {
        parameter.Value = value.Value;
    }

    public override OrderId Parse(object value)
    {
        return new OrderId((Guid)value);
    }
}

You would just need to register the custom handler with Dapper using SqlMapper.AddTypeHandler(new OrderIdTypeHandler());

A full example implementation

I've been dribbling bits of implementation out in this post, so below is a full example implementation for an imaginary FooId type, including custom JsonConverter and a custom TypeConverter:

[JsonConverter(typeof(FooIdJsonConverter))]
[TypeConverter(typeof(FooIdTypeConverter))]
public readonly struct FooId : IComparable<FooId>, IEquatable<FooId>
{
    public Guid Value { get; }

    public FooId(Guid value)
    {
        Value = value;
    }

    public static FooId New() => new FooId(Guid.NewGuid());
    public static FooId Empty { get; } = new FooId(Guid.Empty);

    public bool Equals(FooId other) => this.Value.Equals(other.Value);
    public int CompareTo(FooId other) => Value.CompareTo(other.Value);

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        return obj is FooId other && Equals(other);
    }

    public override int GetHashCode() => Value.GetHashCode();

    public override string ToString() => Value.ToString();
    public static bool operator ==(FooId a, FooId b) => a.CompareTo(b) == 0;
    public static bool operator !=(FooId a, FooId b) => !(a == b);

    class FooIdJsonConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var id = (FooId)value;
            serializer.Serialize(writer, id.Value);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var guid = serializer.Deserialize<Guid>(reader);
            return new FooId(guid);
        }

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(FooId);
        }
    }
    
    class FooIdTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var stringValue = value as string;
            if (!string.IsNullOrEmpty(stringValue)
                && Guid.TryParse(stringValue, out var guid))
            {
                return new FooId(guid);
            }

            return base.ConvertFrom(context, culture, value);

        }
    }
}

That's a lot of boilerplate code!

Yes, I know. That's a lot of code for a simple ID type. I still think it's worth it, but there's no denying it's verbose…

All the F# devs shouting at the screen right now…

To try and counteract that somewhat, I've created a Visual Studio Snippet as described in the docs. Copy the XML below into a file (or download the snippet from here) and import it into your IDE.

Once it's imported, you can type typedid, hit Tab twice, type a new name for the ID and auto-generate all of that code! Note that you may need to add Newtonsoft.Json as a NuGet reference to your project if it's not already referenced.

Screencast of the snippet in action

The snippet - feel free to customize as you see fit!

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Strongly Typed ID</Title>
      <Description>Create a strongly typed ID struct</Description>
      <Shortcut>typedid</Shortcut>
      <HelpUrl>https://andrewlock.net/using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-2/</HelpUrl>
    </Header>
    <Snippet>
      <Declarations>
        <Literal>
          <ID>TypedId</ID>
          <ToolTip>Replace with the name of your type</ToolTip>
          <Default>TypedId</Default>
        </Literal>
      </Declarations>
      <Code Language="csharp"><![CDATA[[JsonConverter(typeof($TypedId$JsonConverter))]
    [TypeConverter(typeof($TypedId$TypeConverter))]
    public readonly struct $TypedId$ : IComparable<$TypedId$>, IEquatable<$TypedId$>
    {
        public Guid Value { get; }

        public $TypedId$(Guid value)
        {
            Value = value;
        }

        public static $TypedId$ New() => new $TypedId$(Guid.NewGuid());
        public static $TypedId$ Empty { get; } = new $TypedId$(Guid.Empty);

        public bool Equals($TypedId$ other) => this.Value.Equals(other.Value);
        public int CompareTo($TypedId$ other) => Value.CompareTo(other.Value);

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            return obj is $TypedId$ other && Equals(other);
        }

        public override int GetHashCode() => Value.GetHashCode();

        public override string ToString() => Value.ToString();
        public static bool operator ==($TypedId$ a, $TypedId$ b) => a.CompareTo(b) == 0;
        public static bool operator !=($TypedId$ a, $TypedId$ b) => !(a == b);

        class $TypedId$JsonConverter : JsonConverter
        {
            public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
            {
                var id = ($TypedId$)value;
                serializer.Serialize(writer, id.Value);
            }

            public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
            {
                var guid = serializer.Deserialize<Guid>(reader);
                return new $TypedId$(guid);
            }

            public override bool CanConvert(Type objectType)
            {
                return objectType == typeof($TypedId$);
            }
        }
        
        class $TypedId$TypeConverter : TypeConverter
        {
            public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
            {
                return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
            }

            public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
            {
                var stringValue = value as string;
                if (!string.IsNullOrEmpty(stringValue)
                    && Guid.TryParse(stringValue, out var guid))
                {
                    return new $TypedId$(guid);
                }

                return base.ConvertFrom(context, culture, value);

            }
        }
    }]]>
      </Code>
      <Imports>
        <Import>
          <Namespace>System</Namespace>
        </Import>
        <Import>
          <Namespace>System.ComponentModel</Namespace>
        </Import>
        <Import>
          <Namespace>System.Globalization</Namespace>
        </Import>
        <Import>
          <Namespace>Newtonsoft.Json</Namespace>
        </Import>
      </Imports>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Summary

Strongly-typed IDs can help avoid simple, but hard-to-spot, argument transposition errors. By using the types defined in this post, you can get all the benefits of the C# type system, without making your APIs clumsy to use, or adding translation code back-and-forth between Guids and your strongly-typed IDs. In this post I showed how to create a custom TypeConverter and a custom JsonConverter for your types. Finally, I provided a complete example implementation, and a Visual Studio snippet for generating strongly-typed IDs in your own project.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?