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
returnsInput
orOutput
according to the data flow being validated (see below). Typically, it isInput
for input parameters and property setters andOutput
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");
}
}
}
}