Home > Blockchain >  c# check value at design time
c# check value at design time

Time:05-14

Is there any way, perhaps by use of Contracts, to check whether a variable's value conforms to some rules during design time in Visual Studio 2022?

I.e. int is being used a lot in C# but quite often what we really want is uint, which is to say only positive values and the standard is to check if a negative value is passed to throw an error/exception/etc.

But would there be some way to write a method and tell Visual Studio or the compiler or whatever "this argument must be >= 0" and whenever a developer passes a value that is less than 0 an error be displayed in the Visual Studio list of errors?

CodePudding user response:

Normally Code Contracts would be perfect for this - but seeming as the feature depressingly died unloved and abandoned in Microsoft's Great Leap Forward from .NET Framework to .NET Core, it means some alternative approach is required.

While it's entirely possible to basically reimplement Code Contracts using Roslyn Code Analysis, it is no small undertaking and will likely take you many months to build an analyzer that can provably verify the possible runtime bounds of a variable through its lifetime (which is, mathematically speaking, impossible to solve in the general case).

So the alternative is to use a refinement type / predicate-type / dependent-type (I forget the exact differences between them) - but the overall gist is that you use a new, distinct type to represent the constraints on its contained value. As C# is statically-typed, it means that types can be used to represent runtime state invariants.

In C#, these types are usually implemented as immutable readonly struct types because there's (usually) zero overhead. You can also combine this with operator overloading, IEquatable<T>, extension-methods, scoped out params, and implicit conversions to make for a very ergonomic refinement-type experience.

(This is where I really pity Java users, as Java doesn't have value-types, nor operator overloading, nor extension methods, nor user-defiend implicit conversions - ouch).

Note: when defining implicit conversions it's very important that you only define implicit conversion from the (narrow) refined type back up to the wider type (as that's always going to succeed) - and you must never define implicit conversion from a wider type to the constrained type because if the wider value is invalid then that will cause a runtime exception when your validation code complains, which the compiler won't be able to pick-up on.


So in your case, you want a type to represent a positive, non-zero Int32 value - so you'd want something like this:

(This code omits implementing the struct/IEquatable<> boilerplate that VS loves to complain about - but it's included in the *.snippet version below).

public static class PositiveInt32Extensions
{
    public static Boolean IsPositive( this Int32 candidate, out PositiveInt32 value ) => PositiveInt32.TryCreate( candidate, out value );
}

public readonly struct PositiveInt32
{
    public static Boolean TryCreate( Int32 candidate, out PositiveInt32  value )
    {
        if( candidate > 0 )
        {
            value = new PositiveInt32( candidate );
            return true;
        }
        else
        {
            value = default;
            return false;
        }
    }

    private readonly Int32 value;

    public PositiveInt32( Int32 value )
    {
        if( value < 1 ) throw new ArgumentOutOfRangeException( nameof(value), actualValue: value, message: "Value must be positive." );
        this.value = value;
    }

    public static implicit operator Int32( PositiveInt32 self ) => self.value;

    // NOTE: This implicit conversion will fail when `unsignedValue  > UInt32.MaxValue / 2`, but I assume that will never happen.
    public static implicit operator PositiveInt32 ( UInt32 unsignedValue ) => new PositiveInt32( (Int32)unsignedValue );
}

Here's my own personal refinement-type *.snippet for Visual Studio - I hope it works for you:


<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<!--

Usage/installation instructions:
1. Save to a file `refine.snippet` somewhere (e.g. in `C:\Users\You\Documents\Visual Studio {year}\Code Snippets\Visual C#\My Code Snippets`).
   * If saved outside your `Visual Studio {year}` folder, or if it isn't detected, add it manually via <kbd>Tools > Code Snippets Manager...</kbd> (Tip: ensure the top "Language" drop-down says "CSharp" as it defaults to ASP.NET for some reason).
2. To try it out, open a .cs file and move your cursor/caret to inside a `namespace`, then type the word "`refine`" and IntelliSense should list it as a snippet in the completion-list popup.
   * If it doesn't appear despite being recognized by Code Snippets Manager ensure VS is configured to list Snippets in the code completion list (Tools > Options > Text Editor > C# > IntelliSense > Snippets behavior > "Always include snippets").
3. Press <kbd>Tab</kbd> once or twice (it varies...) and it should be inserted, with the caret moved to the first `$refinementname$` placeholder. Type the new value then press <kbd>Tab</kbd> to move to the $supertypename$ placeholder. Press <kbd>Tab</kbd> or <kbd>Enter</kbd> when done.

-->

    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>refine</Title>
            <Shortcut>refine</Shortcut>
            <SnippetTypes>
                <!-- There are only 2 types of Snippets: "Expansion" and "Surround-With", btw: https://docs.microsoft.com/en-us/visualstudio/ide/code-snippets?view=vs-2022 -->
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
            <Description>Refinment type represented by a public readonly struct with implicit conversion support.</Description>
        </Header>
        <Snippet>
            <Declarations>
                <Object Editable="true">
                    <ID>refinementname</ID>
                    <Type>Object</Type>
                    <ToolTip>PascalCased summary of the refinement type's predicate - this is concatenated with $supertype$ for the final struct name. e.g. &quot;ValidatedEmailAddress&quot; (&quot;ValidatedEmailAddressUser&quot;)</ToolTip>
                    <Default>NewRefinement</Default>
                </Object>
                <Object Editable="true">
                    <ID>supertype</ID>
                    <Type>Object</Type>
                    <ToolTip>The name of the type that is being refined. e.g. &quot;User&quot; (for &quot;ValidatedEmailAddressUser&quot;)</ToolTip>
                    <Default>SupertypeName</Default>
                </Object>
            </Declarations>
            <!-- Inside <Code>, the only reserved-token names are `$end$` and `$selected$`. Both can only be used at-most once. -->
            <!-- BTW, for this snippet specifically, should the `.Value` property's getter self-validating? or always trust the constructor instead? What's the best way to prevent `default(StructType)` thesedays? -->
            <Code Language="CSharp" Kind="type decl"><![CDATA[
    public static partial class RefinementExtensions
    {
        public static Boolean Is$refinementname$$supertype$( this $supertype$ value, [NotNullWhen(true)] out $refinementname$$supertype$? valid )
        {
            return $refinementname$$supertype$.TryCreate( value, out valid );
        }

        /// <summary>Throws <see cref="ArgumentException"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        /// <exception cref="ArgumentException"></exception>
        public static $refinementname$$supertype$ To$refinementname$$supertype$( this $supertype$ value )
        {
            return $refinementname$$supertype$.Create( value );
        }
    }

    public readonly struct $refinementname$$supertype$ : IReadOnly$supertype$, IEquatable<$refinementname$$supertype$>, IEquatable<$supertype$>
    {
        #region Create / TryCreate

        /// <summary>Throws <see cref="ArgumentException"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        /// <exception cref="ArgumentException"></exception>
        public static $refinementname$$supertype$ Create( $supertype$ value )
        {
            if( TryCreate( value, out $refinementname$$supertype$? valid ) ) return valid.Value;
            else throw new ArgumentException( paramName: nameof(value), message: "Argument object does not satisfy "   nameof($refinementname$$supertype$)   "'s refinement predicate." );
        }

        /// <summary>Returns <see langword="null"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        public static $refinementname$$supertype$? TryCreate( $supertype$ value )
        {
            return TryCreate( value, out $refinementname$$supertype$? valid ) ? valid : null;
        }

        /// <summary>Returns <see langword="false"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        public static Boolean TryCreate( $supertype$ value, [NotNullWhen(true)] out $refinementname$$supertype$? valid )
        {
            if( CONDITION )
            {
                valid = new $refinementname$$supertype$( value );
                return true;
            }

            return false;
        }

        #endregion

        public static implicit operator $supertype$( $refinementname$$supertype$ self )
        {
            return self.Value;
        }

        private $refinementname$$supertype$( $supertype$ value )
        {
            this.value_doNotReadThisFieldExceptViaProperty = value ?? throw new ArgumentNullException(nameof(value));
        }

        private readonly $supertype$ value_doNotReadThisFieldExceptViaProperty;

        public $supertype$ Value => this.value_doNotReadThisFieldExceptViaProperty ?? throw new InvalidOperationException( "This "   nameof($refinementname$$supertype$)   " value is invalid." );

        public override String ToString() => this.Value.ToString();

        #region IReadOnly$supertype$

        // TODO?

        #endregion

        #region IEquatable<$refinementname$$supertype$>, IEquatable<$supertype$>

        private Boolean IsDefault => this.value_doNotReadThisFieldExceptViaProperty is null;

        public override Boolean Equals( Object? obj )
        {
            if( this.IsDefault )
            {
                return obj is null;
            }
            else if( obj is $supertype$ super )
            {
                return this.Equals( super: super );
            }
            else if( obj is $refinementname$$supertype$ other )
            {
                return this.Equals( other: other );
            }
            else
            {
                return false;
            }
        }

        public Boolean Equals( $refinementname$$supertype$ other )
        {
            return ( this.IsDefault && other.IsDefault ) || ( this.Value == other.Value );
        }

        public Boolean Equals( $supertype$? super )
        {
            return !this.IsDefault && ( this.Value == super );
        }

        public override Int32 GetHashCode()
        {
            if( this.IsDefault ) return 0;

            return this.Value.GetHashCode(); // return HashCode.Combine( this.Value );
        }

        public static Boolean operator ==( $refinementname$$supertype$ left, $refinementname$$supertype$ right )
        {
            return left.Equals( other: right );
        }

        public static Boolean operator !=( $refinementname$$supertype$ left, $refinementname$$supertype$ right )
        {
            return !left.Equals( other: right );
        }

        #endregion

    }$end$]]>
            </Code>
            <Imports>
                <Import>
                    <Namespace>System</Namespace>
                </Import>
                <Import>
                    <Namespace>System.Diagnostics.CodeAnalysis</Namespace>
                </Import>
            </Imports>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>
  • Related