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 Order
s, containing a single Post
action for creating Order
s.
[ApiController]
public class OrderController : ControllerBase
{
[HttpPost]
public IActionResult Post(Order order);
}
As we have a strongly-typed IDs Order
s and User
s, the Order
object now looks something like the following (instead of using Guid
s 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 Guid
s:
- Override
CanConvert
: This converter can only convertOrderId
types. - Override
WriteJson
: To serialize anOrderId
, extract theGuid Value
, and serialize that. - Override
ReadJson
: Start by deserializing aGuid
, and create anOrderId
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"}
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 string
s. 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 TypeConverter
s before!
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.
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, argumentation 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 Guid
s 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.