16

I've been thinking about creating custom types for identifiers like this:

public enum CustomerId : int { /* intentionally empty */ }
public enum OrderId : int { }
public enum ProductId : int { }

My primary motivation for this is to prevent the kind of bug where you accidentally pass an orderItemId to a function that was expecting an orderItemDetailId.

It seems that enums work seamlessly with everything I would want to use in a typical .NET web application:

  • MVC routing works fine
  • JSON serialization works fine
  • Every ORM I can think of works fine

So now I am wondering, "why shouldn't I do this?" These are the only drawbacks I can think of:

  • It may confuse other developers
  • It introduces inconsistency in your system if you have any non-integral identifiers.
  • It may require extra casting, like (CustomerId)42. But I don't think this will be an issue, since the ORM and MVC routing will typically be handing you values of the enum type directly.

So my question is, what am I missing? This is probably a bad idea, but why?

  • When are enums NOT a code smell? – gnat Feb 2 '16 at 18:49
  • 3
    This is called "microtyping", you can google around (e.g. this is for Java, details are different, but motivation same) – qbd Feb 2 '16 at 19:22
  • I've typed up two partial answers and then deleted them because I realized I was thinking about this wrong. I agree with you that "This is probably a bad idea, but why?" – user2023861 Feb 2 '16 at 20:41
7

It's a great idea to create type for every identifier kind, mainly for the reasons you stated. I wouldn't do that by implementing the type as enum because making it a proper struct or class gives you more options:

  • You can add implicit conversion to int. When you think about it, it's the other direction, int -> CustomerId, that is the problematic one that deserves explicit confirmation from the consumer.
  • You can add business rules that specify what identifiers are acceptable and what are not. It's not just about a weak check whether the int is positive: sometimes, the list of all possible IDs is kept in database, but changes only during deploy. Then you can easily check that given ID exist against cached result of the proper query. This way, you can guarantee that your application will fail fast in the presence of invalid data.
  • Methods on the identifier can help you with doing expensive DB-existence checks or obtaining the corresponding entity/domain object.

You can read about implementing this in the article Functional C#: Primitive obsession.

  • 1
    i'd say you almost certainly so not want to wrap each int in a class but in a struct – jk. Feb 5 '16 at 12:52
  • Good remark, I updated the answer slightly. – Lukáš Lánský Feb 5 '16 at 13:22
3

It is a clever hack, but still a hack in that is abuses a feature for something which it is not the intended purpose. Hacks have a cost in maintainability, so it is not enough you can't find any other drawbacks, it also need to have significant benefits compared to the more idiomatic way to solve the same problem.

The idiomatic way would be to create wrapper type or struct with a single field. This will give you the same type safety in a more explicit and idiomatic way. While your proposal is admittedly clever, I don't see any significant advantages to using wrapper types.

  • 1
    The main advantage of an enum is that it "just works" the way you want it to with MVC, serialization, ORMs, etc. I have created custom types (structs and classes) in the past and you end up writing more code than you should have to in order to make those frameworks/libraries work with your custom types. – default.kramer Feb 5 '16 at 17:18
  • Serialization should work transparently either way, but for eg. ORM's you do need to configure some kind of explicit conversion. But this is the price of a strongly typed language. – JacquesB Feb 6 '16 at 13:09
2

From my POV, the idea is good. The intention to give different types to unrelated classes of identifiers, and otherwise express semantics via types and let the compiler check it, is a good one.

I don't know how it works in .NET performance-wise, e.g. are the enum values necessarily boxed. OTOH code that works with a database is likely I/O-bound anyway and boxed identifiers are not going to be any bottleneck.

In a perfect world, identifiers would be immutable and only comparable for equality; actual bytes would be an implementation detail. Unrelated identifiers would have incompatible types so you couldn't use one for another by mistake. Your solution practically fits the bill.

-1

You cannot do this, enums must be predefined. So unless you are willing to pre-define the entire spectrum of possible values of customer ids, your type will be useless.

If you cast, you would be defying your primary objective of preventing to accidently assign a type A id to a type B id. So enums are not helpful to your purpose.

This does bring up the question though if it would be helpful to create your types for customer id, order id and product id by descending from Int32 (or Guid). I can think of a reasons why this would be wrong.

The purpose of an id is identification. Integral types are already perfect for this, they do not get any more identifying by descending from them. There is no specialization required nor desired, your world will not be represented better by having different types for identifiers. So from an OO perspective it would be bad.

With your suggestion you want more than type safety, you want value safety and that is beyond the modeling domain, that is an operational issue.

  • 1
    No, enums do not have to be predefined in .NET. And the point is that casting hardly ever happens, e.g. in public ActionResult GetCustomer(CustomerId custId) no cast is needed - the MVC framework will supply the value. – default.kramer Feb 2 '16 at 20:52
  • 2
    I don't agree. For me, it makes perfect sense from OO perspective. Int32 has no semantics, it's purely "technical" type. But I have a need for abstract Identifier type which serves the purpose of identifying objects and which happens to be implemented as Int32 (but could be long or whatever, doesn't really matter). We have concrete specializations like ProductId which descend from Identifier which identify concrete instance of ProductId. It doesn't make any logical sense to assign value of OrderItemId to ProductId (it's clearly an error) so it's great that type system can protect us from this. – qbd Feb 2 '16 at 21:08
  • 1
    I didn't know C# was this flexible regarding the use of enums. It certainly is confusing to me as a developer used to the fact (?) that enums are, well, enumerations of possible values. They typically specify a range of named ids. So now this type is "abused" in the sense that there is no range and there are no names, just the type is left. And apparently, the type isn't that safe either. Another thing: the example is obviously a database application and at some point your "enum" will be mapped to an integral type field at which moment you would lose your presumed type safely after all. – Martin Maat Feb 3 '16 at 5:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.