Metalama / / Conceptual documentation / Creating aspects / Creating simple aspects / Validating parameters, fields and properties
Open sandbox

Getting started: contracts

One of the most popular use cases of aspect-oriented programming is to create a custom attribute to validate 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 whitespace of a string).

Technically speaking, a contract is a piece of code that you inject after receiving or before sending a value. You can do more than throw an exception or normalize the value.

The simple way: overriding the ContractAspect class

  1. Add the Metalama.Framework package to your project.

  2. Create a new class derived from the ContractAspect abstract class. This class will be a custom attribute. It is common to name it with the Attribute suffix.

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

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

    The nameof(value) expression will be replaced with the name of the target 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].

Example: null check

The most common use of contracts is to check nullability. Here is the simplest example.

using Metalama.Framework.Aspects;
using System;

namespace Doc.SimpleNotNull
{

    public class NotNullAttribute : ContractAspect
    {
        public override void Validate( dynamic? value )
        {
            if ( value == null! )
            {
                throw new ArgumentNullException( nameof(value) );
            }
            
        }
    }
}
namespace Doc.SimpleNotNull
{
    public class TheClass
    {
        [NotNull]
        public string Field = "Field";

        [NotNull]
        public string Property { get; set; } = "Property";

        public void Method( [NotNull] string parameter )
        {

        }
    }
    
}
using System;

namespace Doc.SimpleNotNull
{
    public class TheClass
    {


        private string _field = "Field";

        [NotNull]
        public string Field
        {
            get
            {
                return this._field;
            }

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

                this._field = value;
            }
        }

        private string _property = "Property";

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

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

                this._property = value;
            }
        }
        public void Method([NotNull] string parameter)
        {
            if (parameter == null!)
            {
                throw new ArgumentNullException(nameof(parameter));
            }
        }
    }

}

Notice how the nameof(value) expression is replaced by nameof(parameter) when the contract is applied to a parameter.

Example: trimming

You can do more with a contract than throwing an exception. In the following example, the aspect trims whitespace from strings. We add the same aspect to properties and parameters.

using Metalama.Framework.Aspects;

namespace Doc.Trim
{
    internal class TrimAttribute : ContractAspect
    {
        public override void Validate( dynamic? value )
        {
            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}'" );
        }

        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; }
    }

    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   '

Going deeper

If you want to go deeper with contracts, consider jumping to the following articles: