andrewlock / StronglyTypedId

A Rosyln-powered generator for strongly-typed IDs
MIT License
1.52k stars 80 forks source link

Hierarchical IDs? #82

Closed mikechristiansenvae closed 9 months ago

mikechristiansenvae commented 1 year ago

In one of my applications, I have a set of hierarchical IDs.

Take, for example, if I wanted to have a way to uniquely identify an aisle in a store. Without StronglyTypedId, I might do this:

public readonly record struct StoreId(int Id)
{
    public string ToString() => Id.ToString();
}
public readonly record struct SectionId(StoreId Store, int Id)
{
    public int StoreId => Store.Id;
    public string ToString() => $"{StoreId}/{Id}";
}
public readonly record struct AisleId(SectionId Section, int Id)
{
    public int SectionId => Section.Id;
    public int StoreId => Section.StoreId;
    public string ToString() => $"{StoreId}/{SectionId}/{Id}";
}

Each level of the hierarchy provides a different context. A SectionId with an integer value of 1, in StoreId 1 is not the same as a SectionId with an integer value of 1, in StoreId 2

It would be nice if I could do something similar with StronglyTypedId.

NxSoftware commented 1 year ago

You can already do this, I'm doing something similar in one of my private projects.

Using [StronglyTypedId(backingType: StronglyTypedBackingType.String)] would allow you to store the Value as the slash delimited path from your ToString methods and there's nothing stopping you adding additional properties to track the StoreId and SectionId.

The only drawback would be you'd get a constructor that would allow you to create an instance with a raw string value however my PR #79 would resolve this by being able to disable the generation of the constructor.

andrewlock commented 9 months ago

Sorry for the delay with this 😳. I'm currently working on a big redesign of the library in this PR:

The main idea is to make the library much more maintainable while also giving people a mechanism to customise the generated IDs as much as they like. This will make it easy to make changes like this 🙂

mikechristiansenvae commented 9 months ago

For what its worth, I ended up implementing this in my project, but I also needed to have some limited inheritance.

To reuse the example I gave above, suppose I had

public readonly record struct StoreId(int Id)
{
    public string ToString() => Id.ToString();
}
public readonly record struct GroceryStoreId(int Id) : StoreId(Id) // Uh oh!  Can't inherit from a struct!!
{
}

So, to handle inheritance, I needed to use a reference type. But, I did not want to incur the cost of instantiation each time. Additionally, I would want to be able to use ReferenceEquals for speed.

Looking back, I knew that XNamespace and XName atomized their values. So, I lifted XHashtable<TValue> from the dotnet repo (MIT license). I modified XHashtable<TValue> to support keys (and renaming it to IdAtomizer<TKey, TValue>.

Then, I have some abstract base classes:

Then, derived classes would simply need to add a public static Get method (and in the case of integer keys, GUIDs, or other key types that can be sequentially or randomly generated, GetNext) that calls the protected Get method. (This two step process is required so that the constructor on the derived classes can remain private; a creation delegate is passed). For IDs that are parents, helper (instance) methods are created to allow you to get the next (or specific) child ID.

It works well for me. Other people might like the same approach. If you're interested, I can add the IdAtomizer and the base classes to an MR for your review.