15

Hello,

I´m trying to rebuild a discriminated union type in C#.
I always created them with classes like this:

public abstract class Result
{
    private Result() { }

    public sealed class Ok : Result
    {
        public Ok(object result)    // don´t worry about object - it´s a sample
            => Result = result;
        
        public object Result { get; }
    }

    public sealed class Error : Result
    {
        public Error(string message)
            => Message = message;

        public string Message { get; }
    }
}

The problem is that is sooooo much boilerplate code when comparing to F#:

type Result =
    | Ok of result : object
    | Error of message : string

So I tried to rebuild the type with the help of C#9 records.

public abstract record Result
{
    public sealed record Ok(object result) : Result;
    public sealed record Error(string message) : Result;
}

Now it is way less code but now there is the problem that anyone can make new implementations of Result because the record has a public constructor.

Dose anyone have an idea how to restrict the implementations of the root record type?

Thanks for your help and your ideas!

JakobFerdinand
  • 1,087
  • 7
  • 20
  • Does this answer your question? [How do I define additional initialization logic for the positional record?](https://stackoverflow.com/questions/64309291/how-do-i-define-additional-initialization-logic-for-the-positional-record) – d4zed Sep 22 '21 at 12:17
  • 3
    Just add `private Result() { }` constructor yourself? – Evk Sep 22 '21 at 12:17
  • Just adding `private Result() { }` is not possible -> Error: `A constructor declared in a record with parameter list must have 'this' constructor initializer.` – JakobFerdinand Sep 22 '21 at 13:06
  • The code provided in your question should not lead to such error while adding constructor, since this error means `Result` record has another constructor with parameters (like `abstract record Result(string something)`). – Evk Sep 22 '21 at 13:14

3 Answers3

6

I solved it with the help of your comments and this other stackoverflow article.

namespace System.Runtime.CompilerServices
{
    internal static class IsExternalInit { }
}

namespace RZL.Core.Abstractions.DMS
{
    public abstract record Result
    {
        private Result() { }

        public sealed record Ok(object result) : Result;
        public sealed record Error(string message) : Result;
    }
}
JakobFerdinand
  • 1,087
  • 7
  • 20
0

While you can definitely add a private constructor to a record, doing so is probably not what you want. As it does not guarantee that no other type outside your record can inherit it. That's because records always have a generated, protected constructor. Your Result can be simply inherited while having a private constructor:

public abstract record Result
{
    private Result() { }

    public sealed record Ok(object result) : Result;
    public sealed record Error(string message) : Result;
}

public record UnexpectedResult : Result
{
    public UnexpectedResult() : base(new Result.Error("dummy"))
    {
    }
}

// ...

Result result = new UnexpectedResult();

So you can never truly guarantee that an object of type Result will always be either Result.Ok or Result.Error (at least at compile time). You have three options:

  1. Prevent external inheritance at runtime by directly implementing a copy constructor:

    public abstract record Result
    {
        // ...
    
        protected Result(Result _)
        {
            if (this is not Ok && this is not Error)
            {
                throw new InvalidOperationException("External inheritance is not allowed");
            }
        }
    }
    
  2. Accept the fact that your type hierarchy will never be closed,

  3. Use classes.

Pharaz Fadaei
  • 1,605
  • 3
  • 17
  • 28
-1

If you don't want to abstract the record you don't have to:

public record ClientVerificationResponse
{
    protected ClientVerificationResponse(bool succeeded)
    {
        Succeeded = succeeded;
    }

    [MemberNotNullWhen(true, nameof(Credentials))]
    public bool Succeeded { get; init; }

    public ClaimsPrincipal? Credentials { get; init; }

    public static ClientVerificationResponse Success(ClaimsPrincipal claimsPrincipal) => new(true) { Credentials= claimsPrincipal };
    public static ClientVerificationResponse Fail => new(false);
}
Tod
  • 2,070
  • 21
  • 27