Metalama / / Conceptual documentation / Creating aspects / Advising code / Validating parameters, fields and properties

Validating parameter, field, and property values with contracts

In Getting started: contracts, you have learned to create simple contracts by implementing the ContractAspect class.

In this article, we will cover more advanced scenarios.

Accessing the metadata of the field, property, or parameter being validated

Your template code can access its context using the following meta APIs:

  • meta.Target.Declaration returns the target parameter, property, or field.
  • meta.Target.FieldOrProperty returns the target property or field but will throw an exception if the contract is applied to a parameter.
  • meta.Target.Parameter returns the parameter (including the parameter representing the return value) but will throw an exception if the contract is applied to a field or property.
  • meta.Target.ContractDirection returns Input or Output according to the data flow being validated (see below). Typically, it is Input for input parameters and property setters and Output for output parameters and return values.

Contract directions

By default, the ContractAspect aspect applies the contract to the default data flow of the target parameter, field, or property.

The default direction is as follows:

  • for input and ref parameters: the input value,

  • for fields and properties: the assigned value (i.e., the value parameter of the setter),

  • for out parameters and return value parameters, the output value.

To change the filter direction, set the Direction property of the ContractAspect class in the constructor.

To learn about customizing eligibility for different contract directions than the default one, see the remarks on the documentation of the ContractAspect class. To learn about eligibility, visit Defining the eligibility of aspects.

Example: NotNull for output parameters and return values

We have already met this aspect in Getting started: contracts. This example refines the behavior: for the input data flow, an ArgumentNullException is thrown, but for the output flow, we throw a PostConditionFailedException. Notice how we apply the aspect to 'out' parameters and to return values.

using Metalama.Framework.Aspects;
using System;

namespace Doc.NotNull
{
    internal class NotNullAttribute : ContractAspect
    {
        public override void Validate( dynamic? value )
        {
            if ( value == null )
            {
                if ( meta.Target.ContractDirection == ContractDirection.Input )
                {
                    throw new ArgumentNullException( nameof(value) );
                }
                else
                {
                    throw new PostConditionFailedException( $"'{nameof(value)}' cannot be null when the method returns." );
                }
            }
        }
    }
}
using System;

namespace Doc.NotNull
{
    internal class Foo
    {
        public void Method1( [NotNull] string s ) { }

        public void Method2( [NotNull] out string s )
        {
            s = null!;
        }

        [return: NotNull]
        public string Method3()
        {
            return null!;
        }

        [NotNull]
        Warning CS8618: Non-nullable property 'Property' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
        
        public string Property { get; set; }
    }

    public class PostConditionFailedException : Exception
    {
        public PostConditionFailedException( string message ) : base( message ) { }
    }
}
using System;

namespace Doc.NotNull
{
    internal class Foo
    {
        public void Method1([NotNull] string s)
        {
            if (s == null)
            {
                throw new ArgumentNullException(nameof(s));
            }
        }

        public void Method2([NotNull] out string s)
        {
            s = null!;
            if (s == null)
            {
                throw new PostConditionFailedException($"'{nameof(s)}' cannot be null when the method returns.");
            }
        }

        [return: NotNull]
        public string Method3()
        {
            string returnValue;
            returnValue = null!;
            if (returnValue == null)
            {
                throw new PostConditionFailedException($"'{nameof(returnValue)}' cannot be null when the method returns.");
            }
            return returnValue;
        }


        private string _property;

        [NotNull]
        public string Property
        {
            get
            {
                return this._property;
            }

            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }

                this._property = value;
            }
        }
    }

    public class PostConditionFailedException : Exception
    {
        public PostConditionFailedException(string message) : base(message) { }
    }
}

Adding contract advice programmatically

As any advice, you can add a contract to a parameter, field, or property from your aspect's BuildAspect method using the AddContract method.

Note

Provide all contracts to the same method from a single aspect when possible. It has better compile-time performance than using several aspects.

Example: automatic NotNull

The following snippet shows how to automatically add precondition checks for all situations in the public API where a non-nullable parameter could receive a null value from a consumer.

The fabric adds a method-level aspect to all exposed methods. Then, the aspect adds individual contracts using the AddContract method.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Fabrics;
using System;
using System.Linq;

namespace Doc.NotNullFabric
{
    internal class NotNullAttribute : MethodAspect
    {
        public override void BuildAspect( IAspectBuilder<IMethod> builder )
        {
            base.BuildAspect( builder );

            foreach ( var parameter in builder.Target.Parameters.Where(
                         p => p.RefKind is RefKind.None or RefKind.In
                              && p.Type.IsNullable != true
                              && p.Type.IsReferenceType == true ) )
            {
                builder.Advice.AddContract( parameter, nameof(this.Validate), args: new { parameterName = parameter.Name } );
            }
        }

        [Template]
        private void Validate( dynamic? value, [CompileTime] string parameterName )
        {
            if ( value == null )
            {
                throw new ArgumentNullException( parameterName );
            }
        }
    }

    internal class Fabric : ProjectFabric
    {
        public override void AmendProject( IProjectAmender amender )
        {
            amender.Outbound.SelectMany(
                    a => a.Types
                        .Where( t => t.Accessibility == Accessibility.Public )
                        .SelectMany( t => t.Methods )
                        .Where( m => m.Accessibility == Accessibility.Public ) )
                .AddAspect<NotNullAttribute>();
        }
    }
}
namespace Doc.NotNullFabric
{
    public class PublicType
    {
        public void PublicMethod( string notNullableString, string? nullableString, int? nullableInt ) { }
    }
}
using System;

namespace Doc.NotNullFabric
{
    public class PublicType
    {
        public void PublicMethod(string notNullableString, string? nullableString, int? nullableInt)
        {
            if (notNullableString == null)
            {
                throw new ArgumentNullException("notNullableString");
            }
        }
    }
}