Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Transforming Code / Validating Parameters, Fields and Properties

Validating Parameter, Field and Property Values

One of the most popular use cases of aspect-oriented programming is to create a custom attribute that validates the field, property, or parameter to which it is applied. Typical examples are [NotNull] or [NotEmpty].

In Metalama, you can achieve this using a contract. With a contract, you can:

  • throw an exception when the value does not fulfill a condition of your choice, or
  • normalize the received value (for instance trimming the spaces of a string).

Technically speaking, a contract a piece of code that you inject after receiving a value (for input parameters and field/property setters) or sending it (for output parameters and field/property getters). So you can actually do more than throwing an exception or normalizing the value.

The simple way: overriding the ContractAspect class

  1. Add Metalama to your project as described in Installing Metalama: Quick Start.

  2. Create a new class derived from the ContractAspect abstract class. This class will be a custom attribute, so it is a good idea to name it with the Attribute suffix.

  3. Implement the Validate method in plain C#. This method will serve as a template defining the way the aspect overrides the hand-written target method.

    In this template, the incoming value is represented by the parameter name value, regardless of the real name of the field or parameter.

  4. The aspect is a custom attribute. You can add it to any field, property or parameter. To validate the return value of a method, use this syntax: [return: MyAspect].

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.

Example: NotNull

The following aspect throws an exception if the field, property or parameter to which it is applied receives a null value, or if a null value is assigned to an out parameter or to the return value.

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

namespace Doc.NotNull
{
    internal class NotNullAttribute : ContractAspect
    {
        public override void Validate( dynamic? value )
        {
            if ( value == null )
            {
                var parameterName = meta.Target.Declaration switch
                {
                    IParameter parameter => parameter.Name,
                    IFieldOrProperty field => field.Name,

                    // Should not happen.
                    _ => meta.Target.ToString()
                };

                if ( meta.Target.ContractDirection == ContractDirection.Input )
                {
                    throw new ArgumentNullException( parameterName );
                }
                else
                {
                    throw new PostConditionFailedException( $"'{parameterName}' 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]
        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("s");
            }
        }

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

        [return: NotNull]
        public string Method3()
        {
            string returnValue;
            returnValue = null!;
            if (returnValue == null)
            {
                throw new PostConditionFailedException("'<return>' 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("Property");
                }

                this._property = value;
            }
        }
    }

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

Example: Trim

The following aspect normalizes the received value by calling the string.Trim method. The only difficulty is that it needs to choose between value.Trim and the null-conditional value?.Trim according to the nullability of the target.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;

#pragma warning disable IDE0059 // Unnecessary assignment of a value
#pragma warning disable CS8602  // Dereference of a possibly null reference.

namespace Doc.Trim
{
    internal class TrimAttribute : ContractAspect
    {
        public override void Validate( dynamic? value )
        {
            if ( ((IHasType) meta.Target.Declaration).Type.IsNullable.GetValueOrDefault() )
            {
                value = value?.Trim();
            }
            else
            {
                value = value.Trim();
            }
        }
    }
}
using System;

namespace Doc.Trim
{
    internal class Foo
    {
        public void Method1( [Trim] string nonNullableString, [Trim] string? nullableString )
        {
            Console.WriteLine( $"nonNullableString='{nonNullableString}', nullableString='{nullableString}'" );
        }

        public string Property { get; set; }
    }

    internal class Program
    {
        public static void Main()
        {
            var foo = new Foo();
            foo.Method1( "     A  ", "   B " );
            foo.Property = "    C   ";
            Console.WriteLine( $"Property='{foo.Property}'" );
        }
    }
}
using System;

namespace Doc.Trim
{
    internal class Foo
    {
        public void Method1([Trim] string nonNullableString, [Trim] string? nullableString)
        {
            nullableString = nullableString?.Trim();
            nonNullableString = nonNullableString.Trim();
            Console.WriteLine($"nonNullableString='{nonNullableString}', nullableString='{nullableString}'");
        }

        public string Property { get; set; }
    }

    internal class Program
    {
        public static void Main()
        {
            var foo = new Foo();
            foo.Method1("     A  ", "   B ");
            foo.Property = "    C   ";
            Console.WriteLine($"Property='{foo.Property}'");
        }
    }
}
nonNullableString='A', nullableString='B'
Property='    C   '

Contract directions

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

  • 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, see Defining the Eligibility of Aspects.

Adding contract advice programmatically

Just as any advice, you can add a contract to a parameter, field or property from the BuildAspect method of your aspect using the AddContract method.

Note

When possible, provide all contracts to the same method from a single aspect. 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.GetValueOrDefault()
                              && p.Type.IsReferenceType.GetValueOrDefault() ) )
            {
                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.With(
                    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");
            }
        }
    }
}