Designing a ORM Key Generator
I hate databases. Well, not really. What I do hate is the overhead of mapping models to database columns. I don't want to deal with designing my models and then having to keep a mapper in sync with it. So I've done the rational thing and designed yet another object relational mapper.
Let me describe this system using a common example. You have a user object, and each user has a username (unique), a display name and a list of friends.
public class User
{
public required string Username { get; set; }
public required string DisplayName { get; set; }
public List<string> Followers { get; set; }
}
Now typically, you would store this in your database with the Username
as the key. Maybe you put it in a table called Users
. But now how to link this User
object with that table? Maybe you have a UserMapper
or a Mapper<User>
that links them together. Now you have to register all your mappers somewhere, and keep them in sync with your models. What if I am importing the data into another project, and want to look up the model that matches the Users
table. What was it? User
? UserModel
? UserDto
? What is in the Followers
list? Was it display names or usernames?
I present my alternative: StorageKey<T>
. The StorageKey
is effectively a tuple of the object type and the object identity.
public struct StorageKey(Type type, string value)
{
public Type Type { get; } = type;
public string Value { get; } = value;
}
public struct StorageKey<T>(string value) : StorageKey(typeof(T), value)
{
}
Now we can amend our User
type with the StorageKey
type:
public class User
{
public required string DisplayName { get; set; }
public List<StorageKey<User>> Followers { get; set; }
}
We would retrieve our user object from the database using something like this:
var user = storage.Get(new StorageKey(typeof(User), "some_username"));
There is enough information to move the StorageKey
creation into the storage provider, so we could get a nice api like so:
var user = storage.Get<User>("some_username");
Now lets take this concept further.
Key Extension
Let's say our user has a profile object. Easy enough, give the profile a GUID for the id, give the user a profile property, done.
public class Profile
{
public required StorageKey<User> Owner { get; set; }
public required string FullName { get; set; }
public required string TimeZone { get; set; }
public required string ColorScheme { get; set; }
}
public class User
{
...
public required StorageKey<Profile> Profile { get; set; }
}
Since the user and their profile have a 1-1 relationship, we don't really need a seperate id for the profile. We can use the username as the profile id. Since the StorageKey<Profile>
will be a tuple of (Profile, username)
, it won't conflict with the User
storage key, which is (User, username)
.
Now lets say we want to add another type of user to our application: groups. A group is similar to a user, except it is managed by multiple users. Since groups don't have singular accounts associated with them, we can't give them a username, so we assign them a GUID for the identity. Well now we have a problem. How can we associate the profile with the group? Sure we could cast the GUID to a string, but we might have a collision where a users username is a GUID value.
There's a lot of ways to skin this cat. I propose the following:
First, we remove the type argument from the profile Owner
property. This way we don't need to create a shared type between User
and Group
, nor do we need to create two different subclasses of Profile
.
public class Profile
{
public required StorageKey Owner { get; set; }
...
}
Next, we will redefine the StorageKey
as a list of tuples:
public struct StorageKeyPart(Type type, string value)
{
public Type Type { get; } = type;
public string Value { get; } = value;
}
public struct StorageKey(List<StorageKeyPart> parts)
{
public List<StorageKeyPart> Parts { get; } = parts;
public Type Type { get; } = parts[^1].Type;
public static StorageKey Extend(Type type, string value)
=> new StorageKey(parts.Append(new StorageKeyPart(type, value)).ToList());
}
Finally we will redefine the identity of a Profile
as an extension of it's owner.
var profile = new Profile() { ... };
var groupId = Guid.NewGuid();
var profileId = new StorageKey(typeof(Group), Guid.NewGuid())
.Extend(typeof(Profile), "");
storage.Save(profileId, profile);
You could imagine we have some generic implementations and a default value of an empty string for the key extension, to help clean things up:
var profileId = new StorageKey<Group>(Guid.NewGuid()).Extend<Profile>();
The value of the key extensions can also be used in cases where you might have a 1-to-many relationship that requires unique keys for the many, but only at the scope of the 1.
new StorageKey<Group>(Guid.NewGuid()).Extend<Subscriber>(webhookUrl);
Wrap Up
It's not perfect, and it may not even be the right tool for the job, but I enjoy the simplicity and in the few projects I've been using it I really enjoy it. Passing around StorageKey<T>
s instead of ambiguous string
s also helps a ton with readability, especially when it is the return value of a method. You can see my full implementation of StorageKey
in my .NET library here. Subject to change of course.
There's some additional details I didn't cover in this post that I plan to cover in future ones:
- How to serialize the type information
- How to handle foreign keys & transactionality
- How the storage strategy differs between relational and non-relational databases.
I have some solutions to these problems, and look forward to writing them up. Until then!