Avoid custom delegates

Rule ND1300 Design

Prefer generic delegates over custom delegates:

- Action<…> for void return type.

- Func<…> for a return type (last generic argument).

- Predicate<T> for a T instance returning a boolean.

- Expression<…> for function definitions compiled/invoked at runtime, also serializable to remote processes.

They clarify code and remove delegate type maintenance. Do not convert delegates consumed by DllImport extern methods (marshalling issues).

How to fix

Replace custom delegates with the generic delegates shown in the replaceWith column.

Estimated Debt: 5 minutes per custom delegate plus 3 minutes per method using it.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Avoid custom delegates</Name>
// <Id>ND1300:AvoidCustomDelegates</Id>

warnif count > 0 
from t in JustMyCode.Types where t.IsDelegate && !t.IsGeneric

let invokeMethod = (from m in t.Methods where m.SimpleNameEquals("Invoke") select m).Single()
let signature1 = invokeMethod.Name.Substring(
   invokeMethod.SimpleName.Length, 
   invokeMethod.Name.Length - invokeMethod.SimpleName.Length)

// 'ref' 'out' and pointers parameters cannot be supported
where !signature1.Contains("&") && !signature1.Contains("*") 

let signature2 = signature1.Replace("(","<").Replace(")",">")
let signature3 = signature2 == "<>" ? "" : signature2
let resultTypeName = invokeMethod.ReturnType == null ? "????" :
                     invokeMethod.ReturnType.FullName == "System.Void" ? "" :
                     invokeMethod.ReturnType.Name
let replaceWith = 
      resultTypeName == "Boolean" && invokeMethod.NbParameters == 1 ?
   "Predicate" + signature3 : resultTypeName == "" ?
   "Action" + signature3  : invokeMethod.NbParameters ==0 ?
   "Func<" + resultTypeName + ">" :
   "Func" + signature3.Replace(">", "," + resultTypeName + ">")
   
let methodsUser = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t))
          
select new { 
   t, 
   replaceWith,
   methodsUser,
   Debt = (5 + 3*methodsUser.Count()).ToMinutes().ToDebt(),
   Severity = Severity.High
}

//<Expl>
// The *{0}* should be replaced with *{1}*.
// Generic delegates clarify code and 
// reduce delegate type maintenance.
//</Expl>

//<Desc>
// Prefer generic delegates over custom delegates:
//
// - *Action<…>* for *void* return type.
//
// - *Func<…>* for a return type (last generic argument).
//
// - *Predicate<T>* for a *T* instance returning a *boolean*.
//
// - *Expression<…>* for function definitions compiled/invoked at runtime,
// also serializable to remote processes.
//
// They clarify code and remove delegate type maintenance. Do not convert
// delegates consumed by *DllImport* extern methods (marshalling issues).
//</Desc>

//<HowToFix>
// Replace custom delegates with the generic delegates shown in the
// **replaceWith** column.
//
// Estimated Debt: 5 minutes per custom delegate plus 3 minutes per method
// using it.
//</HowToFix>

Design: all 16 items