Dapper And Strongly Typed IDs - How To Dynamically Register Mappings

Dapper and StronglyTypedId are two awesome packages that we can use in our dotnet development -- but how can we combine Dapper and strongly-typed IDs to use them together?

In this article, I'll explain an approach I am building that expands upon the package author's recommendation. While it's not optimized right now, there are two interesting directions to take this in!

Remember to check out these platforms:

// FIXME: social media icons coming back soon!


What Is Dapper?

Dapper is a simple, lightweight ORM (Object-Relational Mapper) for C# developers. Many people are familiar with Entity Framework Core (EF Core), but Dapper is a great alternative that you can use as your ORM. In fact, after using no ORM for years and trying out EF Core, I found myself disappointed... But recently trying Dapper I realized it's exactly what I was after.

Dapper is designed to enhance the performance of data access by providing a fast and efficient way to interact with databases. Instead of masking what SQL looks like behind the in-code APIs, you write the raw SQL and allow Dapper to perform the object mapping for you. However, it looks like Dapper has SQL-building utility classes as well!

Here are some key points when considering using Dapper in your application:

  1. Performance: Dapper is known for its speed. It uses raw SQL queries but maps the results directly to C# objects, making it faster than traditional ORMs like Entity Framework.
  2. Simplicity: Dapper is straightforward to use. Developers can write SQL queries directly and execute them with minimal overhead.
  3. Flexibility: It works with any database that supports ADO.NET, including SQL Server, MySQL, PostgreSQL, SQLite, and more.
  4. Micro ORM: Dapper is considered a micro ORM because it doesn't provide all the features of a full-fledged ORM but focuses on performance and simplicity. It handles basic CRUD operations efficiently.
  5. Extension Methods: It extends the IDbConnection interface with methods like Query and Execute, allowing for easy data retrieval and manipulation.
  6. Minimal Setup: Dapper requires minimal configuration and setup, making it quick to integrate into existing projects.

You can check out more about Dapper by visiting the GitHub page for the project.


What Are Strongly Typed IDs?

Strongly-typed IDs are a technique in C# to improve type safety by replacing primitive types like int, Guid, or string with custom types that represent specific entities' IDs. This helps avoid errors caused by mixing up different types of IDs and enhances code readability and maintainability.

You might be asking how it helps with those things, and the answer is addressing part of "primitive obsession". When we have primitive types representing things with certain meanings, we can run into easy bugs because it's simple to swap a string for another string, or an int for another int. But if one is supposed to be a UserId and the other is supposed to be an AccountType, these should NOT be interchangeable!

Andrew Lock's StronglyTypedId project provides a way to generate strongly-typed ID classes in C#. Andrew Lock is a popular dotnet blogger and this library is absolutely awesome.

Here are some key points to check out when considering using this project:

  1. Type Safety: Strongly-typed IDs ensure that IDs for different entities (e.g., UserId, OrderId) are not accidentally interchanged, reducing bugs.
  2. Code Generation: Andrew Lock's StronglyTypedId project uses source generators to automatically create strongly-typed ID classes, minimizing manual boilerplate code.
  3. Ease of Use: You can define a strongly-typed ID by simply adding an attribute to an entity, and the project handles the rest.
  4. Compatibility: The generated IDs are compatible with common libraries and frameworks, ensuring they work seamlessly within the existing codebase. We'll see more of this coming up soon!
  5. Serialization: Strongly-typed IDs can be easily serialized and deserialized, making them practical for use in APIs and data storage.
  6. Customization: The project allows for customization of the generated ID classes, such as choosing the underlying type (int, Guid, etc.). You're not locked into one primitive backing-type.

If you're looking for a simple way to get better typing support for your IDs, StronglyTypedId by Andrew Lock is awesome! Here's how simple the code is:

[StronglyTypedId(Template.Long)]
public readonly partial struct YourId { }

The Challenge With Dapper and Strongly Typed IDs

The Challenge:

One of Dapper's fundamental responsibilities is to be able to map out objects to data and map data to our objects. At the end of the day, the sources of data that we're dealing with generally deal with (mostly) common primitive types like strings , ints, floats, etc... So as long as the entity you're trying to red and write can be represented by primitive types, you're (mostly) good to go!

However, a strongly-typed ID from Lock's StronglyTypedId package is a struct and a value type, but it's *not* a primitive type. As a result, as soon as you swap your primitives over to using strongly-typed IDs, everything breaks when it comes to the data transforms.

But don't worry! Andrew Lock has a solid solution for this.

The Proposed Solution:

The proposed solution from Andrew Lock can be found on his blog:
https://andrewlock.net/using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-3/#interfacing-with-external-system-using-strongly-typed-ids

In the article, you can see a simple code snippet that we can use to convert a specific StronglyTypedId back-and-forth:

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

From there we just need to wire it up to be used:

SqlMapper.AddTypeHandler(new OrderIdTypeHandler());

The handler implementation has two simple methods for getting the value and for creating an instance of the specific StronglyTypedId.

My Solution for Dapper and Strongly Typed IDs

My Beef With The Original Solution

While Andrew Lock's solution is super simple and effective, there's something that I don't like about it: Every time that I create a new StronglyTypedId, I now need to remember to create a dedicated handler class and wire up this handler class as well.

While for most people this isn't a big deal, this goes against some of the design philosophies I try to have. I don't like creating additional work that needs to be done manually. The reason for this is that it's error-prone and even when it's trivial, it takes some extra time to do it.

I came up with a solution that extends what Andrew Lock has proposed. While my ideal solution doesn't look like it's supported yet, I'll walk you through what I've created as well as the limitations of this approach.

My Proposed Solution

I wanted to see if I could make the work for creating these handlers happen only once. As mentioned previously, my "ideal" solution is not yet functional (seems like we'll need .NET 9.0 support for it), this is still working great for me.

We're going to go with a generic handler:

private sealed class LongStrongTypeHandler<TStrongType>(
    Func<long, object> _createCallback,
    Func<object, long> _getValueCallback) :
    SqlMapper.TypeHandler<TStrongType>
{
    public override TStrongType Parse(object value)
    {
        var castedValue = (long)value;
        var instance = _createCallback.Invoke(castedValue);
        return (TStrongType)instance;
    }

    public override void SetValue(
        IDbDataParameter parameter,
        TStrongType? value)
    {
        parameter.Value = value == null
            ? DBNull.Value
            : _getValueCallback.Invoke(value);
    }
}

If you're reading this and alarm bells are going off, hang tight, because I'll go over this in more detail soon. But as you can see, this is a handler for StronglyTypedIds backed by longs. For my needs, I'm almost only using this data type but could easily add a more generic variation fo this that uses a another type parameter for the backing value type.

In order to use this, we wire things up with the following code:

var typePairs = assemblies
    .SelectMany(assembly => assembly.GetTypes())
    .Select(type =>
    {
        //{[System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta08")]}
        var generatedCodeAttribute = type.GetCustomAttribute<System.CodeDom.Compiler.GeneratedCodeAttribute>();
        if (generatedCodeAttribute is null ||
            generatedCodeAttribute.Tool != "StronglyTypedId")
        {
            return (null, null, null);
        }

        var constructor = type.GetConstructors().Where(x => x.GetParameters().Length == 1).Single();
        var parameter = constructor.GetParameters()[0];

        return (StrongType: type, ValueType: parameter.ParameterType, Constructor: constructor);
    })
    .Where(x => x.ValueType != null && x.StrongType != null)
    .ToArray();

foreach (var type in typePairs)
{
    // NOTE: I only support longs for the stuff in my code
    if (type.ValueType != typeof(long))
    {
        throw new NotSupportedException(
            "Only long StrongTypes are supported so far. Add your own!");
    }

    Type genericClass = typeof(LongStrongTypeHandler<>);
    Type constructedClass = genericClass.MakeGenericType(type.StrongType);

    var getValueMethod = type.StrongType.GetMethod("get_Value");

    var typeHandler = (SqlMapper.ITypeHandler)Activator.CreateInstance(
        constructedClass,
        new object[]
        {
            (long value) => type.Constructor.Invoke([value]),
            (object x) => (long)getValueMethod.Invoke(x, null)
        });

    SqlMapper.AddTypeHandler(type.StrongType, typeHandler);
}

The top portion of the code does assembly scanning to go find types of interest. We need to look for the GeneratedCodeAttribute and match the right tool. From there, we use some bits of reflection to get a constructor and a getter property to be able to create a new instance of our generic handler. Finally, we add it to the SqlMapper class.

You only need to call this code once on startup and it will go register all your handlers as required.

The Glaring Problem And Hopeful Fix

The huge problem with this approach is the potential performance impact. Lock's approach was very simple with respect to types. In this case, I need to be generic AND dynamic, so reflection is used to overcome this.

If performance is absolutely critical, this would be a lackluster option:

  • The handler uses reflection to create the StronglyTypedId instance
  • The handler uses reflection to ask the StronglyTypedId for its value
  • There's boxing (value type to object) with how the handler is setup.

For my current use case, I have no concerns about this though. Performance isn't critical (yet) and I have a line of sight into creating a more performance variation of this. It's what I wanted to start with but hit some snags:

    private sealed class StrongTypeHandler<TStrongType, TValueType> :
        SqlMapper.TypeHandler<TStrongType>
    {
        [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
        private extern static TStrongType CreateTypeInstance(TValueType value);

        [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Value")]
        private extern static TValueType GetValue(TStrongType @this);

        public override TStrongType Parse(object value)
        {
            var castedValue = (TValueType)value;
            var instance = CreateTypeInstance(castedValue);
            return instance;
        }

        public override void SetValue(
            IDbDataParameter parameter,
            TStrongType? value)
        {
            parameter.Value = value == null
                ? DBNull.Value
                : GetValue(value);
        }
    }

This code uses UnsafeAccessor attributes, which can be insanely performant. This article demonstrates just how performant they are, but the benchmarks comparing unsafe access to reflection and direct access are below:

| Method              | Mean       | Error     | StdDev    |
|-------------------- |-----------:|----------:|----------:|
| Reflection          | 35.9979 ns | 0.1670 ns | 0.1562 ns |
| ReflectionWithCache | 21.2821 ns | 0.2283 ns | 0.2135 ns |
| UnsafeAccessor      |  0.0035 ns | 0.0022 ns | 0.0018 ns |
| DirectAccess        |  0.0028 ns | 0.0024 ns | 0.0023 ns |

This data shows clearly my approach is multiple orders of magnitude slower but it also shows that my ideal approach might technically be right on par with direct access. It looks like it might be .NET 9.0 by the time we can use UnsafeAccessor with generics. Another option here is to use source generators the converters so the boilerplate is all done for us. But regardless, there are some options on the horizon when I need to optimize!


Wrapping Up Dapper And Strongly Typed IDs

The StronglyTypedId project by Andrew Lock is awesome, and leveraging Dapper and Strongly Typed IDs only takes a little bit of work. The approach I've outlined in this article shows how we can dynamically scan for handler classes to create. Although this current implementation is lackluster due to reflection, we can look forward to using UnsafeAccessor attributes and/or source generators to optimize this.

If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube! Meet other like-minded software engineers and join my Discord community!

Affiliations

These are products & services that I trust, use, and love. I get a kickback if you decide to use my links. There’s no pressure, but I only promote things that I like to use!

  • BrandGhost: My social media content and scheduling tool that I use for ALL of my content!
  • RackNerd: Cheap VPS hosting options that I love for low-resource usage!
  • Contabo: Affordable VPS hosting options!
  • ConvertKit: The platform I use for my newsletter!
  • SparkLoop: Helps add value to my newsletter!
  • Opus Clip: Tool for creating short-form videos!
  • Newegg: For all sorts of computer components!
  • Bulk Supplements: Huge selection of health supplements!
  • Quora: I answer questions when folks request them!

Frequently Asked Questions: Dapper And Strongly Typed IDs

TBD

How to Master the Art of Reflection in CSharp and Boost Your Programming Skills

Learn about reflection in CSharp and how it can be used. See how reflection in C# allows you to explore and modify objects, classes, and assemblies at runtime.

Autofac ComponentRegistryBuilder in ASP.NET Core - How To Register Dependencies (Part 3)

Learn how to use Autofac ComponentRegistryBuilder in ASP.NET Core! We'll see how we can move closer to getting the C# plugin architecture support we want!

Blazor RenderFragment - How To Use Plugins To Generate HTML

In this article, we'll see how we can use an ASP.NET Core Blazor RenderFragment alongside plugins to dynamically load HTML into our applications!

An error has occurred. This application may no longer respond until reloaded. Reload x