csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Proposal: Type aliases / abbreviations / newtype

Open louthy opened this issue 7 years ago • 45 comments

F# has a feature called type abbreviations where a more complex type can have an easier to use alias. I am finding more and more (especially as I use structs as wrapper types to avoid null) that creating sub-classes for more bespoke behaviour is either impossible or unnecessarily heavyweight.

    // Implicit alias
    public implicit alias IntMap<T> = Dictionary<int, T>;

    // Usage
    var x = new IntMap<string>();

    // Explicit alias
    public alias Month = int
        where value >= 1 && value =< 12;

This is similar to the using MyAlias = MyType, but is parametric, generates a new type, and works across assemblies.

For further discussion is the notion of a predicate for the value that is run on construction and can be used to constrain the type further:

    public alias Year = int 
        where value > -5000 && value < 5000;

    public alias Month = int 
        where value >= 1 && value <= 12;

    public alias Day = int 
        where value >= 1 && value <= 31;

    public alias Username = string
        where value.Length > 0;

    public alias Password = string
        where value.Length > 6 && 
              value.Exists(Char.IsLetter) && 
              value.Exists(Char.IsDigit) &&
              value.Exists(Char.IsPunctuation);

The constraint would be injected into the constructor of the generated type alias (which would throw an ArgumentOutOfRange exception if it returns false). And could also be used be tooling to do flow analysis on whether 'bad values' were likely to get into the type.

One technique that I have been using a lot recently is to embed validation into a type. Although I think this is likely to be very niche (because it's bulky to use right now), I think it opens up a ton of useful functionality that is similar to dependently typed languages.

Here's a simple example with a bespoke list implementation. This makes use of the concepts idea that has been previously investigated by the Roslyn team:

    public interface Pred<A>
    {
        bool IsTrue(A value);
    }

    public struct NonEmpty<A> : Pred<IReadOnlyList<A>>
    {
        public bool IsTrue(IReadOnlyList<A> list) => 
            list.Count > 0;
    }

    public struct Any<A> : Pred<IReadOnlyList<A>>
    {
        public bool IsTrue(IReadOnlyList<A> list) => 
            true;
    }

    public struct List<PRED, A> : IReadOnlyList<A>
        where PRED : struct, Pred<IReadOnlyList<A>>
    {
        A[] items;

        public List(IEnumerable<A> items)
        {
            // Initialise the list
            this.items = items.ToArray();

            // Runs the predicate validation
            if(!default(PRED).IsTrue(this)) throw new ArgumentOutOfRangeException(nameof(items));
        }

        // ... Implementation of the IReadOnlyList interface
    }

The PRED generic argument allows for validation behaviour to be injected into the type and the value. And therefore the compiler can do a ton of validation for free:

    public int Product(List<NonEmpty<int>, int> list)
    {
        var result = list.First();
        foreach(var item in list.Skip(1))
        {
            result = result * item;
        }
        return result;
    }

That means Product can't ever get a type that represents an empty list. e.g.

    var x = new List<Any<int>, int>(new int [0]);
    var y = new List<NonEmpty<int>, int>(new int [0]);   // Compiles, but throws at run-time
    var z = new List<NonEmpty<int>, int>(new [] { 1, 2, 3, 4  });

    var res1 = Product(x);        // Invalid type, won't compile
    var res2 = Product(y);        // Won't ever get here because y can't be constructed
    var res3 = Product(z);        // Works

Clearly passing around List<NonEmpty<int>, int> is very annoying, and so a type-alias of:

    public alias NonEmptyList<A> = List<NonEmpty<A>, A>

Would be great, and would make Product much more declarative and easy to parse:

    public int Product(NonEmptyList<int> list)
    {
        var result = list.First();
        foreach(var item in list.Skip(1))
        {
            result = result * item;
        }
        return result;
    }

This is just one example of where this would be useful and lead to more explicit and declarative code (in my humble opinion). Obviously with the constraints it would be possible to do this:

    public alias NonEmptyList<A> = List<A>
        where value.Count > 0;

I would prefer it to go much further than the F# version, and make the alias work more like Haskell's newtype. That is it's essentially not implicitly convertible with the type it's aliasing. That makes it trivial to represent lighterweight concepts like:

    public alias UserId = int where value > 0
    public alias FirstName = string where value.Length > 0
    public alias Surname = string where value.Length > 0

We could then have methods like:

     public User AddUser(UserId id, FirstName first, Surname last) => ...

Which would kill another common set of bugs in C#, namely that int, string, etc. are essentially 'untyped' (they have a type, but the type doesn't represent the data stored within it).

    public DateTime CreateDate(int year, int month, int day) => ...

Can anyone say they've never fallen over on something like this ^^^.

    public alias Year = int
    public alias Month = int
    public alias Day = int

    public DateTime CreateDate(Year year, Month month, Day day) => ...

I think calling new to instantiate an aliased type is fine, but I would prefer this:

    CreateDate(Year(2017), Month(4), Day(5));

To this:

    CreateDate(new Year(2017), new Month(4), new Day(5));

We should allow explicit conversions though:

    CreateDate((Year)2017, (Month)4, (Day)5);

I expect under-the-hood for the compiler to create a new type for this, which should be a lightweight struct:

    public struct NewType<A>
    {
        public readonly A Value;
        public NewType(A value) => Value = constraintExpr
             ? value
             : throw new ArgumentOutOfRangeException(nameof(value));
        public static explicit operator NewType<A>(A value) => new NewType<A>(value);
        public static implicit operator A(NewType<A> value) => value.Value;
    }

So Year would be:

    [AliasOf(typeof(int))]
    public struct Year
    {
        public readonly int Value;
        public Year(int value) => Value = constraintExpr
             ? value
             : throw new ArgumentOutOfRangeException(nameof(value));
        public static explicit operator Year(int value) => new Year(value);
        public static implicit operator int(Year value) => value.Value;
    }

And then the compiler can inject the Value indirection and re-wrap with the single argument constructor. Obviously this comes with a run-time cost, but it is relatively small for such a powerful feature.

The AliasOf attribute could be a hint for tooling.

I'm a strong believer that types should be the guidance to the programmer rather than variable names, because variable names don't persist from the input of a function to the output, whereas types do.

(by the way I'm aware of the limitation of new List<NonEmpty<A>, A>() when the type is a struct, I want to raise that as a separate issue)

louthy avatar Apr 05 '17 16:04 louthy

If I understand correctly, part of what you'd like is already available:

namespace ConsoleApplication1 {
    using UserId = Int16;
    using FirstName = String;
    using SurName = String;
    using IntMapToString = Dictionary<int, string>;

    class Program {
        static void Main(string[] args) {
        }

        void Test(UserId userId, FirstName firstName, SurName surName) {
            // Do stuff here.
        }
    }
}

Using using with generics is not possible though,

Edit: I didn't see mentioned the using statement in the original post. Maybe that was edited later?

sirgru avatar Apr 05 '17 16:04 sirgru

@sirgru That only works within a single compilation unit (a single source file). This should work intra and inter assembly.

louthy avatar Apr 05 '17 16:04 louthy

Yes. I think there could be value in allowing it to be scoped the same way as the namespace and allowing this to compile:

 using IntMap<T> = Dictionary<int, T>;

sirgru avatar Apr 05 '17 17:04 sirgru

In reality it's just really annoying having to specify it for every source file, and has none of the constraints I listed. It's also a major maintenance headache if you want to change the type being aliased. I think most people who use using x = y are using it to avoid naming clashes, not as a more declarative type system.

louthy avatar Apr 05 '17 17:04 louthy

This proposal adds on #259, improving it in a way that would be extremely helpful for me.

Whatever we do here, I hope we don't block a future CLR improvement where I can declare a runtime type alias, enabling me to change the name of a type without breaking source or binary compatibility.

jnm2 avatar Apr 05 '17 17:04 jnm2

@jnm2 What does this add to #259? I see these as being the same. What am I missing?

scottdorman avatar Apr 06 '17 00:04 scottdorman

My preoccupations is they resembles too much C typedef, then when I'll use another library I find an IntMap<T> and how I should know that is a Dictionary with another name?

Calling and int "Year" seems yet more a typedef without any real value something as C "size_t" that in reality is simply an it :-( It will be different if "Day" while being an int (with all operators working!) will have the added value of the validation (only a positive number between 1 and 31).

fanoI avatar Apr 06 '17 08:04 fanoI

I think in addition to the desires of #259 proposal, this proposal brings a couple of functional programming habits into C#. For example, the desire to clearly carve out the limitations of a type and carry them explicitly in the type definition.

IMHO this does not work in C# because all users have to construct that specific limited type for every argument of the methods they are calling. This brings a lot of extra work on the client's side and "over-specification" that does not necessarily bring further clarity. They aim to bring the "documentation of limitations with the type" but it ends up being a burden to bring it everywhere. The classic, dead-simple way to do it, check arguments and bail as soon as they are invalid:

class DateTime {
    public DateTime(int year, int month, int day) {
        if(!IsValidDate(month, day)) throw new System.ArgumentOutOfRangeException("Unexpected value range for month / day.");

        // Do work
    }

    private bool IsValidDate(int month, int day) {
        // Do work
        return true;
    }
}

class User {
    void Operation() {
        var date = new DateTime(year: 2017, month: 4, day: 6);
    }
}

To me, this is 10x clearer because:

  1. The DateTime(year: 2017, month: 4, day: 6); will always avoid the perceived error of mixing the arguments.
  2. Throwing exceptions is the preferred signaling method that something unexpected is happening, with arguments and otherwise. As proposed, the exception is still thrown but the throwing point is in the predicate and not at the natural spot of checking for validity. This could bring less clarity when debugging.
  3. In C#, this does not guard us from doing anything wrong, as mentioned:
var y = new List<NonEmpty<int>, int>(new int [0]);   // Compiles, but throws at run-time 

so, the only purpose of carrying these properties within the type is to carry the documentation with the definition. The exception will be thrown at the call site either way, and no further compile time safety is gained.

If there is a lot of these constraints on a type, then in a general case some other abstraction can be made in an OOP way and the checks can be done there.

I think this proposal sees the bulky-ness of the solutions, and tries to create additional language features to combat that.

sirgru avatar Apr 06 '17 09:04 sirgru

My preoccupations is they resembles too much C typedef, then when I'll use another library I find an IntMap<T> and how I should know that is a Dictionary with another name?

Tooling can help here. A tooltip that shows the alias when you hover over it would solve that issue. F12ing to the definition seems trivial too. This is an issue with all polymorphic types, so I'm not convinced it's a negative against this proposal.

Calling and int "Year" seems yet more a typedef without any real value something as C "size_t" that in reality is simply an int :-(

It isn't simply an int, it's a Year that happens to be backed by an int. It stops programmers accidentally providing a Day where the Year should be, and forces them to be explicit when providing an int.

It will be different if "Day" while being an int (with all operators working!) will have the added value of the validation (only a positive number between 1 and 31).

I agree, see my comment on the Constrained Types proposal. Extending this to contain a predicate of some sort would be great, but I already know there's value to strongly typing simple types as I'm using it with the NewType feature I talk about in that comment. It's also great for Func signatures to describe exactly what the expected parameters are.

    Func<Year, Month, Day, DateTime>

I think a constrained type system should be part of a general type system improvement, rather than something specifically for alias types. But I'd be happy to flesh that out if people thought it would be more appropriate here? (EDIT: Now fleshed out in the original issue)

To me, this is 10x clearer because:

You're taking one example, then using a technique that isn't enforced by the compiler and saying 'this works'. Are you saying you've never been confused over a signature that takes ints, strings, bools, etc.? Have you never had an int input variable get accidentally assigned to the wrong thing mid-method and because there's no compiler errors the value has propagated and caused issues later?

so, the only purpose of carrying these properties within the type is to carry the documentation with the definition.

No, it's to enforce type-safety for the life of the object. The self documenting declarative nature of it is a very, very nice side-effect.

The exception will be thrown at the call site either way, and no further compile time safety is gained.

Having an exception thrown at the source of the error is a most valuable feature. Just like nullable references are walking time-bombs that need to be constantly checked, so are integer and string values, because they don't have a 'context'. The example of a NonEmptyList, you want to know at the source of the instantiation that the type isn't going to work later when it's propagated through myriad functions.

louthy avatar Apr 06 '17 11:04 louthy

I have added some example syntax for how a possible constrained alias might work:

    public alias Year = int 
        where value > -5000 && value < 5000;

    public alias Month = int 
        where value >= 1 && value <= 12;

    public alias Day = int 
        where value >= 1 && value <= 31;

    public alias NonEmptyList<A> = List<A>
        where value.Count > 0;

    public alias Username = string
        where value.Length > 0;

    public alias Password = string
        where value.Length > 6 && 
              value.Exists(Char.IsLetter) && 
              value.Exists(Char.IsDigit) &&
              value.Exists(Char.IsPunctuation);

The idea would be that the where expression is injected into the constructor for the new type, and would throw an ArgumentOutOfRange error if the expression returns false.

louthy avatar Apr 06 '17 11:04 louthy

That sounds an awful lot like the contracts proposal(s).

yaakov-h avatar Apr 06 '17 12:04 yaakov-h

What I was trying to say is that for the vast majority of general cases this kind of overt argument specification isn't needed, in Visual Studio there's the popup which displays the name of the next argument so the chance of such error is really low in this day and age. Where there is lack of clarity for the reader, named arguments can be used.

The non-null reference types are coming in the next versions of C#. I can understand the need to put these base types in context, but if the constraints are tied to the type then the new type can be created manually just like you've shown above. I believe most of the arguments in most APIs are user-defined types, and where the arguments are base types they can have specific constraints on the constructor, and all further work is not with base types but the "now properly constructed" type. I would find it superfluous to have a Year type whose only purpose is to be a type for the constructor at one place, I would rather have the checks inside the constructor and work with Date from there. Same general logic can apply in other cases.

Just my opinion though.

sirgru avatar Apr 06 '17 12:04 sirgru

@sirgru I think you're possibly thinking a bit too narrowly about short term usage of base types. Of course we can do this validation manually every time a value is used, like we do with null reference checking. But I'm thinking more about types that have a context (like Month), but once they're stored as an int that information is lost; the name of a variable can't enforce the rules of the type, no matter how obvious you think it is at the point of use. A good example is the common error in javascript with zero indexed months. That's an example of where an out-by-one error could propagate.

Another example that gets away from what might seem like the trivial Year, Month, Day examples is in the project I run day to day (which is a multimillion line code-base, and therefore cognitively challenging to keep in the mind constantly), we'll use aliases for Id types for ORM classes. So for example PersonId is aliased to an int, CompanyId also. That means that the common issue of int IDs flying about doesn't end up with the wrong ID in the wrong foreign-key field. This can be especially problematic if you have a function that takes multiple IDs, and a later refactor causes the order of arguments to change.

This is common thinking in functional languages, where the types are the 'point of truth' and not relying on imperatively injected validation throughout an app. The type system validates much more at compile time rather than relying purely on runtime checks (which may be missing, and are therefore impossible for the compiler to reason about).

louthy avatar Apr 06 '17 12:04 louthy

@scottdorman #259 is explicitly asking for aliases internal to an assembly; my only need is for aliases that act externally.

jnm2 avatar Apr 06 '17 12:04 jnm2

One major problem with this proposal is that sometimes type predicates are dependent on others, e.g. Day should be dependent on Month and Year in the example given in this proposal.

A more common example is Index:

public alias NonNegative = int where value >= 0;
public alias Index<T>(ICollection<T> collection) = NonNegative
    where value <= collection.Count;

void Foo(IList<T> list, Index(list) index) { ... }

Where you want it to use the collection Count rather than just being non-negative.

DerpMcDerp avatar Apr 07 '17 21:04 DerpMcDerp

I really like this feature but it could be achieved by Source Code Generator as I listed in Code Generator Catalog.

On the other hand, if you want constraints on types, "defaultability" checking might be also needed.

ufcpp avatar Apr 08 '17 02:04 ufcpp

@ufcpp Source generators don't help when what you're doing is using type aliases to rename an existing type without breaking binary compatibility of assemblies built against that type.

jnm2 avatar Apr 08 '17 13:04 jnm2

@DerpMcDerp

One major problem with this proposal is that sometimes type predicates are dependent on others, e.g. Day should be dependent on Month and Year in the example given in this proposal.

That isn't a problem with this proposal, it's a problem with the example in this proposal. What you're suggesting is a much more complicated proposal which is more akin to dependently types languages. I'm not saying that's not desired (I'd love it), but it's out of the scope of this relatively simple proposal.

louthy avatar Apr 10 '17 16:04 louthy

Overall I think this would make code much easier to understand and like how it introduces some additional type safety. I see the value of creating new "primitive" types with built-in validation that match the real-world domain. Although you can validate method parameters and throw before stuffing invalid data into a string or int we lose meaning. EmailAddress is easier to grok than string. Custom primitives help solve the "primitive obsession" code smell. Func and Action delegates are easier to understand without parameter names. Tuples are easier to understand without component names. I think there are a few cases:

(1) An alias that is meant to be EXACTLY the same as the type it aliases. It just provides a shorter more intuitive name. Implicit casting to the alias is sometimes desirable like when a method or variable expects a IntMap<T> but you give it a Dictionary<int, T>

(2) A type that RESTRICTS the domain of the other type through its name but NOT through the values it accepts. For example if PersonId and ProductId are both represented by Guid, a method that expects a PersonId should not accept a ProductId without some kind of cast.

(3) A type that RESTRICTS that legal values of the other type like Month = int where value >= 1 && value <= 12. This would be really useful for quickly defining specialized types with custom validation. I'm not sure where you'd draw the line between the quick syntax and having the flexibility of defining additional methods on the new type. If you had alias Farenheit = double you might want a ConvertToCelcius instance method. Maybe you'd want a custom constructor that normalizes a string before storing it. Some of these things could be defined with extension methods and static methods defined elsewhere, but it would be more convenient to define them as part of the alias, like...

public type Email restricts string { 
    static Regex _pattern = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$", RegexOptions.Compiled);

    public Email(string input)
    {
        if (!_pattern.IsMatch(input ?? string.Empty)) throw new ArgumentOutOfRangeException();
        value = input.Trim().ToLower();
    }

    public string UserName => { .. return portion before the domain name... }

    public static (bool isValid, Email result) TryParse(string input) => { .. }
}

jmagaram avatar Apr 27 '17 04:04 jmagaram

Hopefully it will help to solve this spaghetti: Task<Output<ICollection<IUser>>>

because currently available using syntax will look like: using Something = System.Threading.Tasks.Task<Root.Namespace1.Output<Root.Namespace2.ICollection<Root.Namespace3.Membership.IUser>>>;

Ugly...

Type aliases are very required...

ghost avatar May 17 '17 23:05 ghost

+100 for type alias'. Especially for string/int type types.

schotime avatar May 21 '17 22:05 schotime

If somebody means global type aliases.. then it is important to allow them to be nested in class to narrow scope.

ghost avatar May 22 '17 05:05 ghost

I think the simple aliasing (without constrains) can come first. And it should be discussed by language design team meeting as a simple but meaningful feature. Constrained types can be discussed later along with method contracts.

gulshan avatar May 26 '17 14:05 gulshan

I agree with @gulshan, a more restricted feature for aliasing should come first. I imagine it could even avoid creating new types at all, something like this:

public newtype Email : string

public void SendEmail(Email to, IEnumerable<Email> cc, string subject, string body) { ... }

This is equivalent to:

public void SendEmail(
[NewType("Email")] string to, 
[NewType("IEnumerable<Email>")] IEnumerable<string> cc, 
string subject, 
string body)
{ ... }

This way programs compiled by older C# versions will be able to consume the API as is, using strings, but more modern programs will get an error when they try to pass a different newtype (say, Phone) instead of Email.

orthoxerox avatar Sep 13 '17 06:09 orthoxerox

@orthoxerox With that approach, if I upgrade to a new compiler, my code suddenly stops compiling? I think that's not considered acceptable.

svick avatar Sep 13 '17 08:09 svick

~~@svick that's what will happen if you upgrade to the compiler version supporting nullable reference types, too.~~

orthoxerox avatar Sep 13 '17 09:09 orthoxerox

@orthoxerox It won't. Nullable reference types only cause warnings, not errors. And even then, it will be opt-in, based on the current proposal. From https://github.com/dotnet/csharplang/issues/790:

A number of the features described would lead to breaking of existing code in the form of new warnings. The simplest "solution" to this is to simply put all the warnings under a big switch. […]

svick avatar Sep 13 '17 10:09 svick

@svick you got me confused there for a moment and I gave a hasty incorrect reply. There won't be a compiler error in most cases, "original type to newtype" and "newtype to original type" conversions should be implicit. The only error would be a conversion from newtype X to newtype Y, which is only possible to get when upgrading your compiler when you either use an external assembly with newtypes incorrectly (so it's a real bug that you've caught) or when you use two external assemblies and pass a newtype from one of them into another expecting a different newtype (now that's a potential issue).

I'll think about the second pitfall some more.

orthoxerox avatar Sep 13 '17 11:09 orthoxerox

@orthoxerox Ok, now that I understand what you meant, that does sound fairly reasonable.

svick avatar Sep 13 '17 11:09 svick

@louthy IMO, we must have a more declarative way to create primitive types with constraints to be in phase with the swagger specification data types: https://swagger.io/docs/specification/data-models/data-types/ In our side we had to develop our own primitive abstract class to manage this kind of constraints in our custom code generation.

Please see below the abstract class we have created to fulfill our needs.



    public interface IPrimitiveType : IComparable
    {
        object Value { get; }
        Type GenericType { get; }
    }

    public interface IPrimitiveType<T> : IPrimitiveType, IEquatable<T>, IComparable<T>
    {
        new T Value { get; }
    }

    [DataContract]
    [Serializable]
    [TypeDescriptionProvider(typeof(PrimitiveTypeTypeDescriptionProvider))]
    [JsonConverter(typeof(PrimitiveTypeJsonConverter))]
    public abstract class PrimitiveType<TMySelf, T> : IPrimitiveType<T>, IFormattable, IValidatableObject
        where TMySelf : PrimitiveType<TMySelf, T>
        where T : IComparable
    {
        static PrimitiveType()
        {
            var valueType = typeof(T);
            if (!valueType.IsSimpleType())
            {
                throw new ArgumentException("The T parameter must be a simple type.");
            }

            if (valueType != typeof(string) && valueType != typeof(bool) && !typeof(IFormattable).IsAssignableFrom(typeof(T)))
            {
                throw new InvalidOperationException($"Cannot create PrimitiveType<{typeof(T).Name}> because it's not implementing {nameof(IFormattable)}.");
            }
        }

        /// <summary>
        /// Override this constructor with [JsonConstructor] attribute
        /// to allow deserialisation of PrimitiveTypes with values
        /// that are no longer possible throught normal constructor validation
        /// </summary>
        protected PrimitiveType() { }

        protected PrimitiveType(T value)
        {
            if (value == null) throw new ArgumentNullException(nameof(value));
            if ((value is Guid || value is DateTime) && value.IsNullOrDefault()) throw new ArgumentNullException(nameof(value));

            Value = value;

            var validationResults = Validate(null).ToArray();

            if (validationResults.Length == 0) return;
            if (validationResults.Length == 1) throw new ValidationException(validationResults[0].ErrorMessage);
            else throw new AggregateException(validationResults.Select(r => new ValidationException(r.ErrorMessage)));
        }

        public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            yield break;
        }

        /// <summary>
        /// Return the validation context display name or name of the type.
        /// </summary>
        protected string SayMyName(ValidationContext validationContext) => validationContext.SayMyName(this);

        [NotNull]
        [DataMember]
        public T Value { get; private set; }

        #region IPrimitiveType Members

        object IPrimitiveType.Value => Value;

        [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "F.Y.")]
        Type IPrimitiveType.GenericType => typeof(T);

        #endregion

        public override bool Equals(object other)
        {
            if (other == null)
            {
                return false;
            }
            if (other is T)
            {
                return this.Value.Equals(other);
            }
            if (!(other is TMySelf))
            {
                return false;
            }
            if (this.Value == null)
            {
                return (other == null);
            }
            return this.Value.Equals(((TMySelf)other).Value);
        }

        public override int GetHashCode()
        {
            return this.Value.GetHashCode();
        }

        public override string ToString() => Value.ToString();

        public string ToString(string format, IFormatProvider formatProvider)
        {
            return (Value as IFormattable)?.ToString(format, formatProvider) ?? ToString();
        }

        public int CompareTo(object obj)
        {
            var implementation = obj as TMySelf;
            if (implementation != null)
            {
                return this.Value.CompareTo(implementation.Value);
            }
            else
            {
                return this.Value.CompareTo(obj);
            }
        }

        public virtual bool Equals(T other)
        {
            return this.Equals((object)other);
        }

        public virtual int CompareTo(T other)
        {
            return this.Value.CompareTo(other);
        }

        public static bool operator !=(PrimitiveType<TMySelf, T> a, PrimitiveType<TMySelf, T> b)
        {
            return !(a == b);
        }
        public static bool operator ==(PrimitiveType<TMySelf, T> a, PrimitiveType<TMySelf, T> b)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(a, b))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            return a.Equals(b);
        }

        public static bool operator !=(PrimitiveType<TMySelf, T> a, T b)
        {
            return !(a == b);
        }
        public static bool operator ==(PrimitiveType<TMySelf, T> a, T b)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(a, b))
            {
                return true;
            }
            if (a == null
                 && (b is Guid && ((Guid)(object)b) == default(Guid)))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }


            return a.Equals(b);
        }

        #region DO NOT USE IMPLICT OR YOU ARE GOING TO A WORLD OF PAIN!

        public static explicit operator T(PrimitiveType<TMySelf, T> PrimitiveType)
        {
            return PrimitiveType.Value;
        }

        public static explicit operator PrimitiveType<TMySelf, T>(T primitive)
        {
            if (primitive == null || (primitive is Guid && ((Guid)(object)primitive) == default(Guid)))
            {
                return null;
            }

            var ths = (TMySelf)Activator.CreateInstance(typeof(TMySelf), primitive);
            return ths;
        }

        #endregion
    }


We use it like this:


  [Serializable]
    [DataContract]
    public class UserId : PrimitveType<UserId, Guid>
    {
        public UserId(Guid value) : base(value)
        {
        }
    }


 [DataContract]
    [Serializable]
    public class Percent : PrimitveType<Percent, decimal>
    {
        public Percent(decimal value) : base(value)
        {
        }

        public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Value < 0 || Value > 100)
                yield return new ValidationResult($"{SayMyName(validationContext)} is not a valid percentage.");
        }
    }

As you can see we had some trouble in the json serialization, we apply the validation after the deserialization is done to avoid error management during the deserialization and response the list of errors in case of bad request.

Of course this code is incomplete, if you need more info don't hesitate to ask me.

JohnnyMaxK avatar May 11 '18 13:05 JohnnyMaxK