4 ms·
In C#, I often use a type like: readonly struct Id32<M> { public readonly int Value { get; } } Then you can do: public sealed class MFoo { } publ
by tyleo 1y ago
In C#, I often use a type like:
readonly struct Id32<M> {
public readonly int Value { get; }
}
Then you can do:
public sealed class MFoo { }
public sealed class MBar { }
And:
Id32<MFoo> x;
Id32<MBar> y;
This gives you integer ids that can’t be confused with each other. It can be extended to IdGuid and IdString and supports new unique use cases simply by creating new M-prefixed “marker” types which is done in a single line.
I’ve also done variations of this in TypeScript and Rust.
- SideburnsOfDoom 1y agoThere are libraries for that, such as Vogen https://github.com/SteveDunn/Vogen https://github.com/SteveDunn/Vogen The name means "Value Object Generator" as it uses Source generation to generate the "Value object" types. That readme has links to similar libraries and further reading.
- tyleo 1y agoThis seems like overkill. I’d prefer the few lines of code above to a whole library.
- SideburnsOfDoom 1y agoIs it "overkill" if it's already written and tested? Once you have several of these types, and they have validation and other concerns then the cost-benefit might flip. FYI, In modern c#, you could try using "readonly record struct" in order to get lots of equality and other concerns generated for you. It's like a "whole library" but it's a compiler feature.
- tyleo 1y agoYes: more code to compile, more stuff to learn, more complexity. I gave like a 5-line-of-code example, I don’t understand why I’d want to replace that with a library.
- SideburnsOfDoom 1y agoI completely agree that libraries do have to prove their worth, and that you should not add them as though they are all zero-cost and zero weight - that is not true. However I disagree in this case - if you have the problem that the library solves and it is ergonomic, then why not use it. Your "5-line-of-code example" doers not cover validation, and serialisation and casting concerns. As another commenter put it: "a properly constructed ID type has a non-trivial amount of code". If you don't need more lines of code than that, then do your thing. But in the example that I looked at, I definitely would. As I said elsewhere in the thread, it is where all customer ids are strings, but only very specific strings are customer ids. The larger point is that people who write c# and are reading this thread should know that these toolkits exist - that url links to other similar libraries and further reading. So they can can then make their own informed choices.
- rjbwork 1y agoHave you used this in production? It seems appealing but seems so anti-thetical to the common sorts of engineering cultures I've seen where this sort of rigorous thinking does not exactly abound.
- SideburnsOfDoom 1y agoSadly I have not. I have played with it and it seems to hold up quite well. I want it for a case where it seems very well suited - all customer ids are strings, but only very specific strings are customer ids. And there are other string ids around as well. IMHO Migration won't be hard - you could allow casts to/from the primitive type while you change code. Temporarily disallowing these casts will show you where you need to make changes. I don't know yet how "close to the edges" you would have to go back to the primitive types in ordered for json and db serialisation to work. But it would be easier to get in place in a new "green field" codebase. I pitched it as a refactoring, but the other people were well, "antithetical" is a good word.
- deleted 1y ago[deleted]
- vborovikov 1y agoSource generators hide too many details from the user. I prefer to have the generated code to be the part of the code repo. That's why I use code templates instead of source generators. But a properly constructed ID type has a non-trivial amount of code: https://github.com/vborovikov/pwsh/blob/main/Templates/ItemTemplates/C%23/ObjectID/ObjectId.cs https://github.com/vborovikov/pwsh/blob/main/Templates/ItemT...
- SideburnsOfDoom 1y ago> a properly constructed ID type has a non-trivial amount of code That is correct, I've looked at the generated code and it's non-trivial, especially when validation, serialisation and casting concerns are present. And when you have multiple id types, and the allowed casts can change over time (i.e. lock it down when the migration is complete) That's why I'd want it to be common, tested code.
- default-kramer 1y agoI've done something like that too. I also noticed that enums are even lower-friction (or were, back in 2014) if your IDs are integers, but I never put this pattern into real code because I figured it might be too confusing: https://softwareengineering.stackexchange.com/questions/309081/what-are-the-drawbacks-to-mapping-integral-identifiers-to-enums https://softwareengineering.stackexchange.com/questions/3090...
- gpderetta 1y agoFWIW, I extensively use strong enums in C++[1] for exactly this reason and they are a cheap simple way to add strongly typed ids. [1] enum class from C++11, classic enums have too many implicit conversions to be of any use.
- TuxSH 1y ago> classic enums have too many implicit conversions They're fairly useful still (and since C++11 you can specify their underlying type), you can use them as namespaced macro definitions
- TuxSH 1y ago> classic enums have too many implicit conversions They're fairly useful still (and since C++11 you can specify their underlying type), you can use them as namespaced macro definitions Kinda hard to do "bitfield enums" with enum class
- gpderetta 1y agoit is not really hard, you need to define the bitwise operators. It would be nice if they could be defaulted.
- yawaramin 1y agoThis technique is called 'phantom type' because no values of MFoo or MBar exist at runtime.