Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strongly typed Guid as generic struct

I already make twice same bug in code like following:

void Foo(Guid appId, Guid accountId, Guid paymentId, Guid whateverId) { ... }  Guid appId = ....; Guid accountId = ...; Guid paymentId = ...; Guid whateverId =....;  //BUG - parameters are swapped - but compiler compiles it Foo(appId, paymentId, accountId, whateverId); 

OK, I want to prevent these bugs, so I created strongly typed GUIDs:

[ImmutableObject(true)] public struct AppId {     private readonly Guid _value;      public AppId(string value)     {                     var val = Guid.Parse(value);         CheckValue(val);         _value = val;     }            public AppId(Guid value)     {         CheckValue(value);         _value = value;                }      private static void CheckValue(Guid value)     {         if(value == Guid.Empty)             throw new ArgumentException("Guid value cannot be empty", nameof(value));     }      public override string ToString()     {         return _value.ToString();     } } 

And another one for PaymentId:

[ImmutableObject(true)] public struct PaymentId {     private readonly Guid _value;      public PaymentId(string value)     {                     var val = Guid.Parse(value);         CheckValue(val);         _value = val;     }            public PaymentId(Guid value)     {         CheckValue(value);         _value = value;                }      private static void CheckValue(Guid value)     {         if(value == Guid.Empty)             throw new ArgumentException("Guid value cannot be empty", nameof(value));     }      public override string ToString()     {         return _value.ToString();     } } 

These structs are almost same, there is a lot of duplication of code. Isn't is?

I cannot figure out any elegant way to solve it except using class instead of struct. I would rather use struct, because of null checks, less memory footprint, no garbage collector overhead etc...

Do you have some idea how to use struct without duplicating code?

like image 225
Tomas Kubes Avatar asked Dec 12 '18 17:12

Tomas Kubes


2 Answers

First off, this is a really good idea. A brief aside:

I wish C# made it easier to create cheap typed wrappers around integers, strings, ids, and so on. We are very "string happy" and "integer happy" as programmers; lots of things are represented as strings and integers which could have more information tracked in the type system; we don't want to be assigning customer names to customer addresses. A while back I wrote a series of blog posts (never finished!) about writing a virtual machine in OCaml, and one of the best things I did was wrapped every integer in the virtual machine with a type that indicates its purpose. That prevented so many bugs! OCaml makes it very easy to create little wrapper types; C# does not.

Second, I would not worry too much about duplicating the code. It's mostly an easy copy-paste, and you are unlikely to edit the code much or make mistakes. Spend your time solving real problems. A little copy-pasted code is not a big deal.

If you do want to avoid the copy-pasted code, then I would suggest using generics like this:

struct App {} struct Payment {}  public struct Id<T> {     private readonly Guid _value;     public Id(string value)     {                     var val = Guid.Parse(value);         CheckValue(val);         _value = val;     }      public Id(Guid value)     {         CheckValue(value);         _value = value;                }      private static void CheckValue(Guid value)     {         if(value == Guid.Empty)             throw new ArgumentException("Guid value cannot be empty", nameof(value));     }      public override string ToString()     {         return _value.ToString();     } } 

And now you're done. You have types Id<App> and Id<Payment> instead of AppId and PaymentId, but you still cannot assign an Id<App> to Id<Payment> or Guid.

Also, if you like using AppId and PaymentId then at the top of your file you can say

using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App> 

and so on.

Third, you will probably need a few more features in your type; I assume this is not done yet. For example, you'll probably need equality, so that you can check to see if two ids are the same.

Fourth, be aware that default(Id<App>) still gives you an "empty guid" identifier, so your attempt to prevent that does not actually work; it will still be possible to create one. There is not really a good way around that.

like image 131
Eric Lippert Avatar answered Sep 19 '22 15:09

Eric Lippert


We do the same, it works great.

Yes, it's a lot of copy and paste, but that is exactly what code-generation is for.

In Visual Studio, you can use T4 templates for this. You basically write your class once and then have a template where you say "I want this class for App, Payment, Account,..." and Visual Studio will generate you one source code file for each.

That way you have one single source (The T4 template) where you can make changes if you find a bug in your classes and it will propagate to all your Identifiers without you having to think about changing all of them.

like image 37
nvoigt Avatar answered Sep 20 '22 15:09

nvoigt