Is it possible to create a C# record with a private constructor?
Asked Answered
F

3

21

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! 😀 💡

Frasier answered 22/9, 2021 at 12:10 Comment(4)
Does this answer your question? How do I define additional initialization logic for the positional record?Burnout
Just add private Result() { } constructor yourself?Finish
Just adding private Result() { } is not possible -> Error: A constructor declared in a record with parameter list must have 'this' constructor initializer.Frasier
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)).Finish
F
8

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;
    }
}
Frasier answered 22/9, 2021 at 13:35 Comment(0)
S
2

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 copy 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.

Stemma answered 5/8, 2023 at 17:56 Comment(0)
H
-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);
}
Hiller answered 15/2, 2023 at 12:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.